I'm relatively new to TypeScript/Javascript/Node.js, but I have to execute a binary myapp (or myapp.exe on Windows) after I have sourced an environment script myapp_env (run myapp_env.bat on Windows)
. myapp_env && myapp
or in windows (cmd)
myapp_env.bat && myapp.exe
I am trying to use spawn:
import {exec,spawn} from 'child_process';
import {exists} from 'fs'
let programhome: string = 'C:/SoftwareAG105/Apama';
let envscript: string = programhome + '/bin/apama_env.bat';
let program: string = programhome + '/bin/correlator.exe';
exists(envscript , found =>
console.log( envscript + (found ? " is there" : " is not there")));
exists(program , found =>
console.log( program + (found ? " is there" : " is not there")));
try {
let test = spawn(envscript + ' && ' + program);
test.stdout.on('data', data => console.log(data));
test.stderr.on('data', data => console.log(data));
test.on('error', data => console.log('ERROR ' + data));
test.on('end', data => console.log('END ' + data));
test.on('exit', data => console.log('Exit ' + data));
} catch (error) {
console.log(error);
}
I get an ENOENT error which I presume is due to it trying to see whether the entire string exists as a file(?). If I run them individually then it works fine. In both cases the exists line will print "is there"....
Edit
After samuels answer I changed the following lines lines
...
import {parse,format,join} from 'path'
...
let programhome: string = join( 'C:' , 'SoftwareAG105', 'Apama' );
let envscript: string = join( programhome ,'bin','apama_env.bat');
let program: string = join(programhome , 'bin' , 'correlator.exe');
exists(envscript , found => console.log( envscript + (found ? " is there" : " is not there")));
exists(program , found => console.log( program + (found ? " is there" : " is not there")));
ERROR Error: spawn C:\SoftwareAG105\Apama\bin\apama_env.bat &&
C:\SoftwareAG105\Apama\bin\correlator.exe ENOENT index.js:15
C:\SoftwareAG105\Apama\bin\apama_env.bat is there index.js:9
C:\SoftwareAG105\Apama\bin\correlator.exe is there
TLDR; So my question is can I chain commands in spawn so that I can source the environment and run the program?
Ok so finally I found the information buried deep in various google posts :
I found that exec would work the way I wanted, but spawn would not, it turns out that for exec a shell is started allowing the chaining to occur.
https://www.freecodecamp.org/news/node-js-child-processes-everything-you-need-to-know-e69498fe970a/
By default, the spawn function does not create a shell to execute the
command we pass into it. This makes it slightly more efficient than
the exec function, which does create a shell. The exec function has
one other major difference. It buffers the command’s generated output
and passes the whole output value to a callback function (instead of
using streams, which is what spawn does).
child_process.spawn takes an options object which can have a property called shell
let test = spawn(envscript + ' && ' + program,{shell:true});
This extra configuration allows me to do the chaining I needed and so I can now source the environment and run the program correctly.
That might have something to do with the path delimiter on Windows being a backslash instead of a forward slash.
When building your strings, try to use path.delimiter (imported from the default node path module) (which is either / or \ depending on the OS). Like so :
let programhome: string = ['C:', 'SoftwareAG105', 'Apama'].join(path.delimiter);.
This way your path will always be valid, regardless of the OS.
Related
I've got a NodeJS (Electron) client which is running the following code:
child = spawn("powershell.exe",['-ExecutionPolicy', 'ByPass', '-File', require("path").resolve(__dirname, '../../../../updater.ps1')]);
child.on("exit",function(){
require('electron').remote.getCurrentWindow().close();
});
The file that this opens is a powershell file which downloads and unpacks an update. If I run this file manually, I get the powershell console which shows me a progress bar for the download and the unpacking. However, running it from code like above does not show the console.
How can I make my code show the powershell console during it's runtime? I'm having a hard time formulating search terms to find an answer to this.
Things I've tried:
Adding '-NoExit' to my 2nd parameter array
Adding { windowsHide: false } parameter
Adding '-WindowStyle', 'Maximized' to 2nd parameter array
I've also tried switching to exec.
exec('powershell -ExecutionPolicy Bypass -File ' + updater_path, function callback(error, stdout, stderr){
console.log(error);
});
Which runs the file but still doesn't show the console.
An answer will preferably allow me to run powershell files un-attached to the NodeJS client, and will also show the powershell console while running.
Here is my current code:
updater = spawn("powershell.exe",['-ExecutionPolicy', 'ByPass', '-File', remote.app.getAppPath() + '\\app\\files\\scripts\\' + data.type + '_updater.ps1'], { detached: true, stdio: 'ignore' });
updater.unref();
Which actually does nothing, it doesn't even seem like it runs the script at all.
I've tried the same thing using a batch file, it's never opened.
updater = spawn("cmd",[remote.app.getAppPath() + '\\app\\files\\scripts\\launch_updater.bat'], { detached: true, stdio: ['ignore', 'ignore', 'ignore'] });
updater.unref();
The shell:true and detached:true where enough to open PowerShell.
I've made this simple demo for a detached PowerShell window:
root:
index.js ( aka your main js file )
powerShell.js ( the detached powershell window)
showTime.ps1 ( aka your file )
In powerShell.js will find that i separated the arguments that keep the PS Window open under the defaultPowerShellArguments variable.
Advice: I see alot of powershell at the spawn command parameter, i cannot stress this enough, add the .exe extension too when running under Windows, correct ispowershell.exe. If you happen to use just powershell and nodepad will all of a sudden pop-up echoing your script, you will have a hard time trying to figure it out.
index.js
const powerShell = require('./powerShell');
let seconds = 5;
let remaining = seconds;
let nd = new Date().getTime() + (seconds * 1000);
setInterval(() => {
remaining -= 1
let time = new Date();
console.log('Your code running at:', time.toTimeString());
if (remaining === 3) {
console.log('Opening powershell...')
powerShell([
"-File",
"./showTime.ps1"
]);
}
if (time.getTime() > nd) {
console.log('Main script will exit.');
process.exit(0);
}
}, 1000);
powerShell.js
module.exports = (spawnArguments) => {
if (typeof spawnArguments === "undefined" || !(spawnArguments instanceof Array)) {
spawnArguments = [];
}
const {spawn} = require('child_process');
let defaultPowerShellArguments = ["-ExecutionPolicy", "Bypass", "-NoExit",];
let powershell = spawn('powershell.exe', [...defaultPowerShellArguments, ...spawnArguments], {
shell: true,
detached: true,
// You can tell PowerShell to open straight to the file directory
// Then you can use the relative path of your file.
// cwd: 'ABSOLUTE_PATH_TO_A_VALID_FOLDER'
// cwd: 'C:/'
});
}
showTime.ps1
Get-Date -Format G
I suspect you'll need to pass the shell option to tell node which shell to execute the command on. This defaults to process.env.ComSpec on Windows, which if for whatever reason isn't set will then default to cmd.exe.
Reference: https://nodejs.org/api/child_process.html#child_process_default_windows_shell:
Although Microsoft specifies %COMSPEC% must contain the path to 'cmd.exe' in the root environment, child processes are not always subject to the same requirement. Thus, in child_process functions where a shell can be spawned, 'cmd.exe' is used as a fallback if process.env.ComSpec is unavailable.
PowerShell shell support has been made available via this commit.
Since the exec command is working, I'd suggest starting there and trying to get the window to show. Try passing shell: 'PowerShell' in the options object. windowsHide defaults to false for child_process.exec, but it might not hurt to pass it as well:
exec('powershell -ExecutionPolicy Bypass -File ' + updater_path, { shell: 'PowerShell', windowsHide: false }, function callback(error, stdout, stderr){
console.log(error);
});
You may also be able to get it working with spawn with similar options: { shell: 'PowerShell', windowsHide: false }
I eventually worked around this by using exec to call a batch file, and this batch file runs the powershell file.
//call buffer .bat file, close main window after 3 seconds to make sure it runs before closing.
exec('start ' + remote.app.getAppPath() + '\\app\\files\\scripts\\launch_updater.bat ' + data.type);
setTimeout(function() {
require('electron').remote.getCurrentWindow().close();
}, 3000);
launch_updater.bat:
#ECHO OFF
set arg1=%~1
start powershell.exe -executionpolicy bypass -File "%~dp0/%arg1%_updater.ps1"
for /f "skip=3 tokens=2 delims= " %%a in ('tasklist /fi "imagename eq cmd.exe"') do (
if "%%a" neq "%current_pid%" (
TASKKILL /PID %%a /f >nul 2>nul
)
)
exit /b
The loop in the batch file is essentially just so it will close itself without leaving a command window open. I pass an argument based on what kind of update it is.
I have a script in the form of a string that I would like to execute in a Node.js child process.
The data looks like this:
const script = {
str: 'cd bar && fee fi fo fum',
interpreter: 'zsh'
};
Normally, I could use
const exec = [script.str,'|',script.interpreter].join(' ');
const cp = require('child_process');
cp.exec(exec, function(err,stdout,sterr){});
however, cp.exec buffers the stdout/stderr, and I would like to be able to be able to stream stdout/stderr to wherever.
does anyone know if there is a way to use cp.spawn in some way with a generic string, in the same way you can use cp.exec? I would like to avoid writing the string to a temporary file and then executing the file with cp.spawn.
cp.spawn will work with a string but only if it has a predictable format - this is for a library so it needs to be extremely generic.
...I just thought of something, I am guessing the best way to do this is:
const n = cp.spawn(script.interpreter);
n.stdin.write(script.str); // <<< key part
n.stdout.setEncoding('utf8');
n.stdout.pipe(fs.createWriteStream('./wherever'));
I will try that out, but maybe someone has a better idea.
downvoter: you are useless
Ok figured this out.
I used the answer from this question:
Nodejs Child Process: write to stdin from an already initialised process
The following allows you to feed a generic string to a child process, with different shell interpreters, the following uses zsh, but you could use bash or sh or whatever executable really.
const cp = require('child_process');
const n = cp.spawn('zsh');
n.stdin.setEncoding('utf8');
n.stdin.write('echo "bar"\n'); // <<< key part, you must use newline char
n.stdout.setEncoding('utf8');
n.stdout.on('data', function(d){
console.log('data => ', d);
});
Using Node.js, it's about the same, but seems like I need to use one extra call, that is, n.stdin.end(), like so:
const cp = require('child_process');
const n = cp.spawn('node').on('error', function(e){
console.error(e.stack || e);
});
n.stdin.setEncoding('utf-8');
n.stdin.write("\n console.log(require('util').inspect({zim:'zam'}));\n\n"); // <<< key part
n.stdin.end(); /// seems necessary to call .end()
n.stdout.setEncoding('utf8');
n.stdout.on('data', function(d){
console.log('data => ', d);
});
I am trying to get the stdout of 7zip when it processes files and get the percentage in nodeJs, but it doesn't behave as expected. 7zip doesn't output anything to stdout until the very end of the execution. Which is not very helpful.. especially when I have large files being compressed and no feedback is shown for a very long time.
The code I am using (simplified):
// 7zip test, place the 7z.exe in the same dir, if it's not on %PATH%
var cp = require('child_process');
var inputFile = process.argv[2]; if(inputFile==null) return;
var regProgress = /(\d{1,3})%\s*$/; //get the last percentage of the string, 3 digits
var proc = cp.spawn("7z.exe",["a","-t7z" ,"-y" ,inputFile + ".7z",inputFile]);
proc.stdout.setEncoding("utf8");
proc.stdout.on("data",function(data){
if(regProgress.test(data))
console.log("Progress = " + regProgress.exec(data)[1] + "%");
});
proc.once("exit",function(exit,sig){ console.log("Complete"); });
I have used the same code to get the percentage with WinRar successfully and I am beginning to think that 7zip might be buggy? Or I am doing it wrong? Can I forcefully read the stdout of a process with a timer perhaps?
The same code above, with the exception of the following line replaced, works as expected with WinRar.
var proc = cp.spawn("Rar.exe",["a","-s","-ma5","-o+",inputFile+".rar",inputFile]);
If anyone knows why this happens and if it is fixable, I would be grateful! :-)
p.s. I have tried 7za.exe, the command line version of 7zip, also the stable, beta and alpha versions, they all have the same issue
It is no longer needed to use a terminal emulator like pty.js, you can pass the -bsp1 to 7z to force to output the progress to stdout.
7-zip only outputs progress when stdout is a terminal.
To trick 7-zip, you need to npm install pty.js (requires Visual Studio or VS Express with Windows SDK) and then use code like:
var pty = require('pty');
var inputFile = process.argv[2],
pathTo7zip = 'c:\\Program Files\\7-Zip\\7z.exe';
if (inputFile == null)
return;
var term = pty.spawn(process.env.ComSpec, [], {
name: 'ansi',
cols: 200,
rows: 30,
cwd: process.env.HOME,
env: process.env
});
var rePrg = /(\d{1,3})%\r\n?/g,
reEsc = /\u001b\[\w{2}/g,
reCwd = new RegExp('^' + process.cwd().replace(/\\/g, '\\\\'), 'm');
prompts = 0,
buffer = '';
term.on('data', function(data) {
var m, idx;
buffer += data;
// remove terminal escape sequences
buffer = buffer.replace(reEsc, '');
// check for multiple progress indicators in the current buffer
while (m = rePrg.exec(buffer)) {
idx = m.index + m[0].length;
console.log(m[1] + ' percent done!');
}
// check for the cmd.exe prompt
if (m = reCwd.exec(buffer)) {
if (++prompts === 2) {
// command is done
return term.kill();
} else {
// first prompt is before we started the actual 7-zip process
if (idx === undefined) {
// we didn't see a progress indicator, so make sure to truncate the
// prompt from our buffer so that we don't accidentally detect the same
// prompt twice
buffer = buffer.substring(m.index + m[0].length);
return;
}
}
}
// truncate the part of our buffer that we're done processing
if (idx !== undefined)
buffer = buffer.substring(idx);
});
term.write('"'
+ pathTo7zip
+ '" a -t7z -y "'
+ inputFile
+ '.7z" "'
+ inputFile
+ '"\r');
It should be noted that 7-zip does not always output 100% at finish. If the file compresses quickly, you may just see only a single 57% for example, so you will have to handle that however you want.
Let's say I'm trying to call ffmpeg from the current directory. On linux I'd call exec or spawn with './ffmpeg'. In order for this code to be portable to windows do I need to strip off the './' or is that somehow taken care of for me?
This is just half an answer: welcome to anyone to modify it and expand it.
Here is the result of my digging in the source code:
The exec method does a small check of the platform (taken from the source):
if (process.platform === 'win32') {
file = 'cmd.exe';
args = ['/s', '/c', '"' + command + '"'];
options = util._extend({}, options);
options.windowsVerbatimArguments = true;
} else {
file = '/bin/sh';
args = ['-c', command];
}
I don't know how you have added ffmpeg to your PATH on Windows, so it really depends on your setup. In any case, simply pass to exec the command as if you would be running it from the cmd.exe shell.
I however could not find something similar for spawn.
Spawn in nodeJS. I have just about managed to use this to run a bash command as follows. This seems to be pretty much non-blocing and I get action on the browser screen as the command trashes through data.
ls = spawn('find',['/'] );
response.writeHead(200, { "Content-Type": "text/plain" });
ls.stdout.on('data', function (data) {
console.log('stdout: ' + data);
response.write(data);
});
But I want to run a perl script with multiple arguments.
ls = spawn('blah.pl',['--argstring here', '--arg blah'] );
Perl script is just written to get arguments using getopts CPAN lib and it using CPAN expect lib to run though a pile of stuff - outputs to stdout and stderr if I have an error but I mostly care about stdout right now.
The thing is this is giving me no output. Seems to be completely blocking at least until the program finishes execution ... and it this case it doesn't at least for 10 mins.
Am I using spawn wrong?
I like the node module "carrier"
carrier = require "carrier"
childproc = require "child_process"
find = childproc.spawn "find"
find.stdout.setEncoding "utf8"
linereader = carrier.carry find.stdout
linereader.on "line", (line) -> console.log line