Never ending shell command with execSync() - node.js

I build VS Code extension
I have wrapper in a class like this
public exec(cmd: string): string {
try {
return execSync(cmd, { cwd: this.workspaceRoot }).toString();
}
catch (e) {
return '' + e;
}
}
If in the code I run
let tags = this.exec('git tag --sort=-v:refname')
I do get a list of tags. Actually, all other commands run also correctly like git status, git config and other. But as soon as I run this.
let res = this.exec(`git push origin ${name}`);
It hangs forever. If I pass wrong tag it stops with error, but if I put correct tag, it suspends. I try to console.log('git push origin ${name}') and then copy result and run that command in a terminal, it runs correctly.
What can be the reason of a such behavior?

Related

NodeJS - How do I detect other copies of my program?

I have written a NodeJS command-line program with two modes:
mode foo: runs forever until the user presses Ctrl+C
mode bar: runs once
If the user is already running the program in mode foo, then running it again in mode bar will cause errors. Thus, when the user invokes mode bar, I want to search for all other existing copies of my command-line program that are running and kill them (as a mechanism to prevent the errors before they happen).
Getting a list of processes in NodeJS is easy, but that doesn't help me much. If I simply kill all other node processes, then I might be killing other programs that are not mine. So, I need to know which specific node processes are the ones running my app. Is it even possible to interrogate a process to determine that information?
Another option is to have my program write a temporary file to disk, or write a value to the Windows registry, or something along those lines. And then, before my program exists, I could clean up the temporary value. However, this feels like a precarious solution, because if my program crashes, then the flag will never be unset and will remain orphaned forever.
What is the correct solution to this problem? How can I kill my own application?
I was able to solve this problem using PowerShell:
import { execSync } from "child_process";
const CWD = process.cwd();
function validateOtherCopiesNotRunning(verbose: boolean) {
if (process.platform !== "win32") {
return;
}
// From: https://securityboulevard.com/2020/01/get-process-list-with-command-line-arguments/
const stdout = execPowershell(
"Get-WmiObject Win32_Process -Filter \"name = 'node.exe'\" | Select-Object -ExpandProperty CommandLine",
verbose,
);
const lines = stdout.split("\r\n");
const otherCopiesOfMyProgram= lines.filter(
(line) =>
line.includes("node.exe") &&
line.includes("myProgram") &&
// Exclude the current invocation that is doing a 1-time publish
!line.includes("myProgram publish"),
);
if (otherCopiesOfMyProgram.length > 0) {
throw new Error("You must close other copies of this program before publishing.");
}
}
function execPowershell(
command: string,
verbose = false,
cwd = CWD,
): string {
if (verbose) {
console.log(`Executing PowerShell command: ${command}`);
}
let stdout: string;
try {
const buffer = execSync(command, {
shell: "powershell.exe",
cwd,
});
stdout = buffer.toString().trim();
} catch (err) {
throw new Error(`Failed to run PowerShell command "${command}":`, err);
}
if (verbose) {
console.log(`Executed PowerShell command: ${command}`);
}
return stdout;
}

Electron app in production mode fails to run external script via child_process.spawnSync API in Mac but works perfectly in Linux

The app computes the sum of the exponentials of the two entered integers using an R code. The inputs are passed in the form of a JSON object to the R code via child_process.spawnSync API of node.js.
The app was packaged using electron-packager(v15.2.0) and its structure is as shown in the screenshot below. Source code to reproduce this issue can be obtained from this GitHub folder: https://github.com/wasimaftab/Utils/tree/master/test_js_r_interaction
index.js file contains the code to interact with R. Important note, you need to install rjson R package before attempting to run the electron app as it is used in R to extract the arguments from json object.
In Ubuntu (18.04) the output as expected, see the screenshot below,
The same code fails in Mac (Catalina 10.15.7) after packaging but, works perfectly in development mode, see the screenshot below.
The actual error is as follows:
Error: spawnSync Rscript ENOENT
at Object.spawnSync (internal/child_process.js:1041:20)
at Object.spawnSync (child_process.js:625:24)
at callSync (file:///Users/admin/Desktop/test_js_r_interaction/release-builds-mac/test_js_r_interaction-darwin-x64/test_js_r_interaction.app/Contents/Resources/app.asar/src/index.js:25:23)
at HTMLButtonElement.<anonymous> (file:///Users/admin/Desktop/test_js_r_interaction/release-builds-mac/test_js_r_interaction-darwin-x64/test_js_r_interaction.app/Contents/Resources/app.asar/src/index.js:84:20)
and the js code to interact with R is as follows:
const path = require("path");
const child_process = require('child_process');
const RSCRIPT = 'Rscript';
const defaultOptions = {
verboseResult: false
}
function parseStdout(output) {
try {
output = output.substr(output.indexOf('"{'), output.lastIndexOf('}"'));
return JSON.parse(JSON.parse(output));
} catch (err) {
return err;
}
}
function callSync(script, args, options) {
options = options || defaultOptions;
const result = args ?
child_process.spawnSync(RSCRIPT, [script, JSON.stringify(args)]) :
child_process.spawnSync(RSCRIPT, [script]);
if (result.status == 0) {
const ret = parseStdout(result.stdout.toString());
if (!(ret instanceof Error)) {
if (options.verboseResult) {
return {
pid: result.pid,
result: ret
};
} else {
return ret;
};
} else {
return {
pid: result.pid,
error: ret.message
};
}
} else if (result.status == 1) {
return {
pid: result.pid,
error: result.stderr.toString()
};
} else {
return {
pid: result.pid,
error: result.stderr.toString()
//error: result.stdout.toString()
};
}
}
I will appreciate any suggestion to fix this issue, thanks in advance
The error you're getting, ENOENT, suggests that your OS cannot find Rscript. This can result from the following scenarios:
Rscript is not even installed.
Rscript is installed, but not executable without a "shell". To check whether it is installed, open a Terminal and execute the command as your Electron application would.
If Rscript can be executed from within a Terminal, there could be something wrong with how the installation has set your paths up. There shouldn't be, really, but it might be necessary to execute spawnSync with additional options, such as { shell: true } to get the correct value of PATH.
If Rscript cannot be executed from within a Terminal, forcing Electron to spawn a shell via the above options (which really is what a Terminal does) will not solve this problem. In this case, try to use the complete path to Rscript as the command instead, if you happen to know where it should be installed.
If neither of those solutions help, try reinstalling Rscript altogether and try again. As I can see nothing which would be wrong with your code, I believe it is a problem of installation.
For more information on child_process.spawnSync (command, args, options), see its documentation.

Husky pre-commit hook finishes after commit

The following script was written in order to sort JSON files by key on pre-commit hook:
/*
* Will reorder all files in given path.
*/
const sortJson = require("sort-json");
const fs = require("fs");
const chalk = require("chalk");
const log = console.log;
const translationsPath = process.argv.slice(2).join(" ");
function readFiles(dirname) {
try {
return fs.readdirSync(dirname);
} catch (e) {
log(chalk.red(`Failed reading files from path; ${e}`));
}
}
log(
chalk.bgGreen(
`Running json sort pre-commit hook on path: ${translationsPath}`
)
);
const files = readFiles(translationsPath);
files.forEach((file) => {
log(chalk.yellow(`Sorting ${file}...`));
try {
sortJson.overwrite(`${translationsPath}\\${file}`);
log(chalk.green(`Finished sorting ${file}`));
} catch (e) {
log(chalk.red(`Failed sorting file ${file}; ${e}`));
}
});
log(
chalk.bgGreen(
`Finished sorting files`
)
);
I'm attaching the script to my package.json with husky precommit hook:
"scripts": {
"sort-translations": "node ./scripts/husky/json-sort src/assets/translations",
...
},
"husky": {
"hooks": {
"pre-commit": "npm run sort-translations"
}
},
The result is that the commit is finished, and only then the script finishes with the created unstage changes.
The script itself runs synchronously with the Finished sorting files message printed last.
My question is, how can I make it synchronous; first finish running node ./scripts/husky/json-sort src/assets/translations, then git commit.
Thanks!
Thanks to #adelriosantiago comment, I figured it out.
First of all, I started using readdirSync() instead of readdir(). Then I was able to verify (through logging) that the script indeed ends only when the files are edited. Unfortunately that wasn't enough - the hook still ended with uncommitted files.
At this point I realized - it's not about the hook, it's about the git staging! The script ends it time, but the modified changes remain unstaged so they're not commited. So I've added && git add src/assets/translations to pre-commit hook.
Now everything works as desired.

Reading an environment variable set by a test in Jenkins pipeline

Not found any reference to this particular question.
I am looking to find a way to achieve something like this in a Jenkins pipeline which runs our acceptance tests using Protractor and Cucumber.js:
steps {
container('selenium') {
script {
try {
{
//run tests
}
}
catch (err) {
if (env.testFailed == 'true') {
println "A test failure exists - build status updated to failure"
currentBuild.result = 'FAILURE'
error "Test(s) have failed"
}
else {
println "No test failures exist - build status updated to success"
currentBuild.result = 'SUCCESS'
}
}
}
}
}
This would fail the build if the env var of testFailed is 'true'. The reason for this is we are encountering bugs with Protractor-Cucumber framework where if a failed test retries and passes the exit code of the stage is still 1.
So in the After hook of each test I am setting the env var using node.js to true if the Scenario status is failed:
if (scenario.result.status === Status.FAILED) {
process.env.testFailed = 'true';
}
if (scenario.result.status === Status.PASSED) {
process.env.testFailed = 'false';
}
The problem I have found is that the Jenkins pipeline fails to read the env var value in the code block of the catch section. It is always null.
Any ideas?
1) change the After hook to write the true/false flag to a file in sync.
2) read the file in catch block
catch(err) {
testFailed = sh(script:'cat result.flag.txt', returnStdout: true).trim()
if(testFailed == 'true') {
...
}
}
Another option if there is total/passed/failed case number in output of npm test
lines = []
try {
lines = sh(script:'npm test', returnStdout: true).readLines();
}
catch(err) {
size = lines.size()
// parse the last 20 lines to extract fail/pass/total number
for(int i=size-20;i<size;i++) {
line[i]
}
}
WHY IT DOESN'T WORK NOW?
I see that you're running your tests in a container. When you set an environment variable, it's reflected on the scope of your container not the Jenkins master server
WHAT YOU COULD TRY TO DO
This actually depends on how you run the tests, but this should be an option
// run tests here
// you should have a variable for your container
def exit_code = sh(script: "sudo docker inspect ${container.id} --format='{{.State.ExitCode}}'", returnStdout: true)
sh "exit ${exit_code}"
This actually also depends how you start the tests inside the container,
So if you update your answer with this information I could help you

Undefined property when unit testing my discord.js bot (the test itself is passed, but it is followed by an error)

I am trying to set up unit testing for my discord.js bot, but when running npm test in the terminal, while the test is being passed, still gives an error.
This is an image of the test being passed followed by the error:
https://i.imgur.com/m2EOuxc.png
I need to fix this error in testing, while still having the bot being able to function.
I have tried to completely remove the line referenced in the error (and the lines that had something to do with that specific line)
jsfiles.forEach((f, i) => {
let props = require(`./cmds/${f}`)
bot.commands.set(props.help.name, props)
})
Removing this resolved the testing issue, but resulted in the bot not functioning correctly (it did not load the commands; meaning, the bot couldn't be interacted with), which is not the goal here.
I've also checked, that each of the files in the folder cmds ends with
module.exports.help = {
name: '<name of the command I use for each command>'
}
This is the part of my bot.js file that contains the problem.
// Loads the commands for the bot:
fs.readdir('./cmds/', (err, files) => {
if (err) console.error(err)
let jsfiles = files.filter(f => f.split('.').pop() === 'js')
if (jsfiles.length <= 0) {
console.log('No commands to load!')
return
}
if (testingSettings) {
console.log(`Loading ${jsfiles.length} commands!`)
}
// This is the problem referenced above:
// ----------------------------------------------------------------------
jsfiles.forEach((f, i) => {
let props = require(`./cmds/${f}`)
bot.commands.set(props.help.name, props)
})
// ----------------------------------------------------------------------
})
This is all of my code in the bot.test.js file
const {
// Functions
checkingTesting,
// Variables
testingSettings,
} = require('./bot')
test('checking to see if testing-mode is on', () => {
expect(checkingTesting(testingSettings, 'token')).toBe(process.env['token']);
});
If it is needed. This is the function, variable and exporting method that is used to connect bot.js to bot.test.js:
Variable (in bot.js file)
const testingSettings = false
Function (in bot.js file)
function checkingTesting (testingSettings, name) {
if (testingSettings) {
return testSettings[name]
} else if (!testingSettings) {
return process.env[name]
}
}
Exporting (in bot.js file)
module.exports = {
// Exporting functions
checkingTesting: checkingTesting,
// Exporting variables
testingSettings: testingSettings,
}
props.help is undefined. The required file's exported obj is either empty, doesn't have help, or some other unforeseen event.
A good practice is to always check whether an object key exist prior using it.
if (props && props.help) {
bot.commands.set(props.help.name, props)
} else {
//throw or handle error here
}
In your command file, it seems like there is no help property of module.exports. When you try to read help.name, it throws your error because help is undefined.
Check to make sure that you're declaring module.exports.help in every command file.

Resources