Can't spawn `gcloud app deploy` from a Node.js script on Windows - node.js

I'm building an Electron application (Node.js) which needs to spawn gcloud app deploy from the application with realtime feedback (stdin/stdout/stderr).
I rapidly switched from child_process to execa because I had some issues on Mac OS X with the child_process buffer which is limited to 200kb (and gcloud app deploy sends some big chunk of string > 200kb which crash the command).
Now, with execa everything seems to work normally on OSX but not on Windows.
The code looks something like this:
let bin = `gcloud${/^win/.test(process.platform) ? '.cmd' : ''}`
//which: https://github.com/npm/node-which
which(bin, (err, fullpath) => {
let proc = execa(fullpath, ['app', 'deploy'], {
cwd: appPath
})
proc.stdout.on('data', data => {
parseDeploy(data.toString())
})
proc.stderr.on('data', data => {
parseDeploy(data.toString())
})
proc.then(() => {
...
}).catch(e => {
...
})
})
This code works perfectly on Mac OS X while I haven't the same result on Windows
I have tried lots of thing:
execa()
execa.shell()
options shell:true
I tried maxBuffer to 1GB (just in case)
It works with detached:true BUT I can't read stdout / stderr in realtime in the application as it prompts a new cmd.exe without interaction with the Node.js application
Lots of child_process variant.
I have made a GIST to show the responses I get for some tests I have done on Windows with basic Child Process scripts:
https://gist.github.com/thyb/9b53b65c25cd964bbe962d8a9754e31f
I also opened an issue on execa repository: https://github.com/sindresorhus/execa/issues/97
Does someone already got this issue ? I've searched around and found nothing promising except this reddit thread which doesn't solve this issue.

Behind the scene, gcloud.cmd is running a python script. After reading tons of Node.js issue with ChildProcess / Python and Windows, I fell on this thread: https://github.com/nodejs/node-v0.x-archive/issues/8298
There is some known issue about running Python scripts from a Node.js Child Process.
They talk in this comment about an unbuffered option for python. After updating the shell script in gcloud.cmd by adding the -u option, I noticed everything was working as expected
This comment explains how to set this option as an environment variable (to not modify the windows shell script directly): https://docs.python.org/2/using/cmdline.html#envvar-PYTHONUNBUFFERED
So adding PYTHONUNBUFFERED to the environment variable fix this issue !
execa(fullpath, ['app', 'deploy'], {
cwd: appPath,
env: Object.assign({}, process.env, {
PYTHONUNBUFFERED: true
})
})

Related

Executing Command in Nodejs and Attaching the output to the console

I'm working on a CLI tool to add an extra layer of automation and utilities to our workflow and I'm wrapping the webpack development command with an alternative in my CLI (here's a demonstration):-
function runDev(){
this.doSomePreAutomationAndPreperations();
this.runWebpackDevCommand();
}
I'm using NodeJs child_proecess.exec and I'm trying to figure out a way to execute the webpack dev command and attach it to the terminal (like -it in docker if you're familiar with it) or transferring the control to the child process(so output will be directly emitted to the console).
Is there away to do that?
It turns out that I can achieve this but just making the child process inherit the stdio. ex:-
const { spawn } = require('child_process')
const shell = spawn('sh',[], { stdio: 'inherit' })
shell.on('close',(code)=>{console.log('[shell] terminated :',code)})

nodejs child_process spawn silently fails on windows

I'm wondering if anyone has had a similar problem with nodejs child_process spawn on windows.
I can no longer execute a nodejs script that calls child_proces.spawn. (This was working fine until yesterday, when suddenly node stopped working properly. No new software installed or anything that I am aware of that could have triggered this.) The call to spawn simply exits the process and fails with no error code, no error message, just exits silently.
My testcase script looks like this:
const { spawn } = require('child_process');
console.log('test 1');
try {
spawn('c:\\windows\\system32\\cmd.exe', ['/d', '/c', 'dir'], { stdio: 'inherit' });
console.log('test 2');
} catch (err) {
console.log('err', err);
}
The output looks like this:
PS C:\test> node .\index.js
test 1
PS C:\test>
Running nodejs v14.17.1 on Windows 10. (I've also tried with nodejs v10, v12, v14.15.1 and v16, both x86 and x64 varieties - there is no difference in behavior.) I've tried uninstalling nodejs and re-installing multiple times. I've tried with Powershell and with the windows Command Prompt.
I had this problem intermittently about 6 months ago and previously just rebooting windows seemed to clear this up. I'm guessing that something has been corrupted in the OS somehow.
Tracing through nodejs through the nodejs debug inspect shows it hits this line and then just quits silently when stepping into this line of code:
https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/child_process.js#L390
const err = this._handle.spawn(options);
I don't see anything odd/wrong in the options object and I've tried with various different environment variable settings for PATH, etc.
UPDATE:
Looks like things work fine if windows is started in safe mode or started with early launch anti-malware protection disabled. Now, I'm suspecting something with anti-virus software is interfering here.
Turns out some new anti-virus rules were blocking all processes attempting to spawn a new cmd.exe child process.

Node's spawn/exec not working when called from a scheduled Windows task

I'm facing a very odd issue where I have a Node script which invokes a process, it looks like this:
// wslPath declared here (it's a shell file)
const proc = cp.spawn('ubuntu.exe', ['run', wslPath]);
let stdout = '';
proc.stdout.on('data', data => stdout += data.toString());
let stderr = '';
proc.stderr.on('data', data => stderr += data.toString());
return await new Promise((resolve, reject) => {
proc.on('exit', async code => {
await fs.remove(winPath);
if (code) {
reject({ code, stdout, stderr });
}
resolve({ stdout, stderr });
});
});
As you can see, the script invokes WSL. WSL is enabled on the computer. When I run this script manually, it works fine. When I log in to the computer the script is at using RDP from another computer and run it with the same credentials, it works fine as well. But when the script is invoked from a scheduled task which also runs with the same credentials, the spawn call returns:
(node:4684) UnhandledPromiseRejectionWarning: Error: spawn UNKNOWN
at ChildProcess.spawn (internal/child_process.js:394:11)
at Object.spawn (child_process.js:540:9)
I verified the user is the same by logging require('os').userInfo() and require('child_process').spawnSync('whoami', { encoding: 'utf8' }) and it returns the same in all three cases.
I assume it is because ubuntu.exe is not being found, but I don't know why that would be as the user is the same in all three cases.
What could be the reason for this and how can I debug this further?
The Windows Task Scheduler allows you to specify a user to run as (for privilege reasons), but does not give you the environment (PATH and other environment variables) that are configured for that user.
So, when running programs from the Windows Task Scheduler, it's important to not make any assumptions about what's in the environment (particularly the PATH). If my program depends on certain things in the environment, I will sometimes change my Task to be a .BAT file that first sets up the environment as needed and then launch my program from there.
Among other things, the simplest way to not rely on the path is to specify the full path to the executable you are running rather than assuming it will be found in the path somewhere. But, you also need to make sure that your executable can find any other resources it might need without any environment variables or you need to configure those environment variables for it before running.

How to change vue-cli message after successfull compile ("App running at...")?

I use vue-cli in my dockerized project, where port mapping looks like this: "4180:8080", and the actual message after compiling my SPA looks like:
App running at:
- Local: http://localhost:8080/app/
It seems you are running Vue CLI inside a container.
Access the dev server via http://localhost:<your container's external mapped port>/app/
App works fine, I could access at via http://localhost:4180/app/ as conceived, but I'm not able to find a proper way to change the message above to show this link instead of "It seems you are running Vue CLI inside a container...". I could use webpack hooks to insert link before the message but actually wanna find the way to change the message, generated by cli. Is it possible somehow?
I came to this question - as I was looking to do the same thing with bash, running inside a Docker container (possibly what you're already doing).
You could achieve this by invoking Vue CLI commands through spawning a child node process, from within your Docker container (assuming your container is running node). You can then modify the output of stdout and stderr accordingly.
You can call a Javascript file in one of two ways:
use a shell script (bash, for example) to call node and run this script
set the entrypoint of your Dockerfile to use this script (assuming you're running node by default)
// index.js
const { spawn } = require('child_process')
const replacePort = string => {
return string.replace(`<your container's external mapped port>`, 8000)
}
const vueCLI = (appLocation, args) => {
return new Promise((resolve, reject) => {
const vue = spawn('vue', args, {cwd: appLocation})
vue.stdout.on('data', (data) => {
console.log(replacePort(data.toString('utf8', 0, data.length)))
})
vue.stderr.on('data', (error) => {
console.log(replacePort(error.toString('utf8', 0, error.length)))
})
vue.on('close', (exitCode) => {
if (exitCode === 0) {
resolve()
} else {
reject(new Error('Vue CLI exited with a non-zero exit code'))
}
})
})
}
vueCLI('path/to/app', CLI_Options).then(() => resolve()).catch(error => console.error(error))
This approach does have drawbacks, not limited to:
performance being slower - due to this being less efficient
potential danger of memory leaks, subject to implementation
risk of zombies, should the parent process die
For the reasons above and several others, this is a route that was found to be unsuitable in my specific case.
Instead of changing the message, it's better to change the port Vue is listening on.
. npm run serve -- --port 4180
This automatically updates your message to say the new port, and after you updated your docker port forward for the new port, it it will work again.

Creating a Visual Studio Code integrated terminal with a custom Node script as the shell

I'm working on a Visual Studio Code extension in which I hope to create a terminal which gives access to a custom shell. I have a Node.js script (.js file) which implements this shell. Now, I'm trying to use Code's createTerminal method from my extension to launch a terminal that uses my Node.js script as its shell.
I can't directly set the shellPath to be my js file, because I have no guarantee that the user has Node.js installed, that the system will run such files with Node.js, nor what version of Node is installed. I need to be able to point at a universally-understood binary which can handle the file. Roughly speaking, I want to do this:
let terminal = vscode.window.createTerminal({
name: "My terminal",
shellPath: 'node', // Use the Node.js executable as the target
shellArgs: [path.join(__dirname, 'my-shell-wrapper.js')] // Tell Node to run my shell
});
terminal.show();
How can I accomplish this? Is there an executable that ships with Code that I can point to which runs Node scripts? Or is there another mechanism which I'm missing?
VSCode now (as of the past year or so) has the option of specifying custom behaviors for terminals aside from just the underlying shell executable. createTerminal has an overload which takes ExtensionTerminalOptions; this has a pty property which allows you to specify custom handlers for reading from and writing to the terminal. From their Pseudoterminal documentation:
const writeEmitter = new vscode.EventEmitter<string>();
const pty: vscode.Pseudoterminal = {
onDidWrite: writeEmitter.event,
open: () => {},
close: () => {},
handleInput: data => writeEmitter.fire(data === '\r' ? '\r\n' : data)
};
vscode.window.createTerminal({ name: 'Local echo', pty });
This can be used to implement arbitrary terminal interactions.

Resources