How do you create a terminal instance within a NodeJS child process? - javascript

I am setting up a discord channel to function as an SSH terminal. A NodeJS server will provide the connection. A custom command will spawn a new terminal instance, which can then be used as a shell.
I don't know how to spawn a terminal within a child process. I have tried using the screen and bash commands to no avail.
I am using CentOS 7.
// Code For Discord
var $discord = {
currentInterface: null,
send: (data) => {
/* some code that sends data to a discord channel */
},
receive: (data) => {
// Send Data To Terminal
if ($discord.currentInterface) {
$discord.currentInterface.send(data);
} else {
$discord.send('**Error:** Terminal has not been spawned.');
}
},
command: (name, args) => {
// Recieve Discord Commands
switch (name) {
case 'spawn':
$discord.currentInterface = $interface();
break;
}
}
};
// Create Interface
var $interface = function () {
// Define object
let x = {
terminal: child_process.spawn('screen'),
send: (data) => {
// Send Input to Terminal
x.process.stdin.write(data + '\n');
},
receive: (data) => {
// Send Output to Discord
$discord.send(data);
}
};
// Process Output
x.terminal.on('stdout', (data) => {
x.receive(data);
});
// Process Errors
x.terminal.on('stderr', (error) => {
x.receive(`**Error:**\n${error}`);
});
// Return
return x;
};
The problem lies with creating the terminal itself. How do you create an SSH-style shell within a child process?

After realizing how much of an idiot I really am, I found a solution...
// Import Modules
const fs = require('fs');
const child_process = require('child_process');
// Create Interface
var interface = {
terminal: child_process.spawn('/bin/sh'),
handler: console.log,
send: (data) => {
interface.terminal.stdin.write(data + '\n');
},
cwd: () => {
let cwd = fs.readlinkSync('/proc/' + interface.terminal.pid + '/cwd');
interface.handler({ type: 'cwd', data: cwd });
}
};
// Handle Data
interface.terminal.stdout.on('data', (buffer) => {
interface.handler({ type: 'data', data: buffer });
});
// Handle Error
interface.terminal.stderr.on('data', (buffer) => {
interface.handler({ type: 'error', data: buffer });
});
// Handle Closure
interface.terminal.on('close', () => {
interface.handler({ type: 'closure', data: null });
});
Usage...
interface.handler = (output) => {
let data = '';
if (output.data) data += ': ' + output.data.toString();
console.log(output.type + data);
};
interface.send('echo Hello World!');
// Returns: data: Hello World!
interface.send('cd /home');
interface.cwd();
// Returns: cwd: /home
interface.send('abcdef');
// Returns: error: bin/sh: line 2: abcdef: command not found
interface.send('exit');
// Returns: exit

I'd take a look at the documentation for child_process.execFile. There's an option to set the shell on, but it's disabled by default.
There's also this approach if you want to try setting up a batch script. This is set up for windows and the answer isn't set up for passing arguments, but you should be able to adapt it fairly easily.

Related

Node.js spawning parent process and capturing subprocess output

const options = {
cwd: process.cwd(),
shell: true,
stdio: 'inherit',
}
const firstCommand = 'pktmon stop'
const secondCommand = 'pktmon filter remove'
const thirdCommand = 'pktmon filter add --ethertype 0x88cc'
const foruthCommand = 'pktmon start --etw --pkt-size 0 --log-mode real-time --comp nics'
const child = require('child_process').spawn(`${firstCommand} && ${secondCommand} &&
${thirdCommand} && ${foruthCommand}`,[], options)
//none of child outputs are triggered by subprocess output
child.stdout.on('data', function(data){
console.log('stdout', data.toString())
})
child.stderr.on('data', function (data) {
console.log('stderr',data.toString())
})
child.on('message', (data) => {
console.log('message',data)
})
Subprocess output is showed on the terminal but how can i actually capture them and save output to a variable?
the reason im using stdio: 'inherit' is because - when i'm not using it the terminal is freezing when last command being executed..

Creating slash commands without options Discord.js V14

In attempt to create slash commands in Discord.JS V14 following the official guide, I came across the following error:
DiscordAPIError[50035]: Invalid Form Body
0[LIST_TYPE_CONVERT]: Only iterables may be used in a ListType
The specific command (ping) that I would like to create doesn't have any additional options as it's a very basic command and simply is just unethical to add any options.
ping.js:
module.exports = {
name: "ping",
description: "View the reaction times",
slashInfo: {
enabled: true,
public: false,
},
getSlashInfo: function() {
const { SlashCommandBuilder } = require("discord.js");
const builder = new SlashCommandBuilder();
// Set basic command information
builder.setName(this.name);
builder.setDescription(this.description);
// If the command can be used in DMs
builder.setDMPermission(true);
// Return the information in JSON format
return builder.toJSON();
},
async execute(interaction, _prefix, client) {
interaction.reply({ content: `**Pong** in ${client.ws.ping}ms` });
}
}
commandHandler.js:
function postSlashCommand(data, to, client) {
if (!to) {
// Post command to all guilds
rest.put(
Routes.applicationCommands(client.user.id),
{ body: data }
);
} else {
// Post command for use only in a specific server
rest.put(
Routes.applicationGuildCommands(client.user.id, to),
{ body: data }
);
}
}
async function setupSlashCommands(directory, client) {
// Loop through known command files
const commandFolders = fs.readdirSync(`./${directory}`).filter(file => !file.endsWith(".js") && !file.endsWith(".json"));
for (const folder of commandFolders) {
const commandFiles = fs.readdirSync(`./${directory}/${folder}`).filter(file => file.endsWith(".js"));
for (const file of commandFiles) {
// Find the command object
const command = require(`../../${directory}/${folder}/${file}`);
// Ensure the command supports slash
if (!command.slashInfo?.enabled) return;
// Get the slash data
let data = command.getSlashInfo();
// Post the command to Discord
if (command.slashInfo.public) { // If the slash command is public
// Post command to all guilds
postSlashCommand(data, null, client);
} else { // If the slash command is in testing
// Post command for use only in the dev server
postSlashCommand(data, require("../../utils/config.json").DevServer, client);
}
}
}
}
module.exports = (client) => setupSlashCommands("commands", client);
Full error:
throw new DiscordAPIError.DiscordAPIError(data, "code" in data ? data.code : data.error, status, method, url, requestData);
^
DiscordAPIError[50035]: Invalid Form Body
0[LIST_TYPE_CONVERT]: Only iterables may be used in a ListType
at SequentialHandler.runRequest (D:\Projects\...\node_modules\#discordjs\rest\dist\lib\handlers\SequentialHandler.cjs:287:15)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async SequentialHandler.queueRequest (D:\Projects\...\node_modules\#discordjs\rest\dist\lib\handlers\SequentialHandler.cjs:99:14)
at async REST.request (D:\Projects\...\node_modules\#discordjs\rest\dist\lib\REST.cjs:52:22) {
rawError: {
code: 50035,
errors: {
_errors: [
{
code: 'LIST_TYPE_CONVERT',
message: 'Only iterables may be used in a ListType'
}
]
},
message: 'Invalid Form Body'
},
code: 50035,
status: 400,
method: 'PUT',
url: 'https://discord.com/api/v10/applications/<application_id>/guilds/<guild_id>/commands',
requestBody: {
files: undefined,
json: {
options: [],
name: 'ping',
name_localizations: undefined,
description: 'View the reaction times',
description_localizations: undefined,
default_permission: undefined,
default_member_permissions: undefined,
dm_permission: true
}
}
}
Is there any way to simply create a slash command without providing the options property?
That endpoint takes an array of application commands. Instead of attempting to put a single command at a time, put all of them at once. Add the data to an array, then you will register them
const globalCommands = [],
guildCommands = [];
// Loop through known command files
const commandFolders = fs.readdirSync(`./${directory}`).filter(file => !file.endsWith(".js") && !file.endsWith(".json"));
for (const folder of commandFolders) {
const commandFiles = fs.readdirSync(`./${directory}/${folder}`).filter(file => file.endsWith(".js"));
for (const file of commandFiles) {
const command = require(`../../${directory}/${folder}/${file}`);
// Ensure the command supports slash
if (!command.slashInfo?.enabled) return;
// Add the slash data to the array
if (command.slashInfo.public) globalCommands.push(command.getSlashInfo());
else guildCommands.push(command.getSlashInfo())
}
}
Afterward, simply put the commands, and with what I see, you want to do global and guild commands, separately
postSlashCommand(guildCommands, require("../../utils/config.json").DevServer, client)
postSlashCommand(globalCommands, null, client)

Is it possible to implement a shared state management for CLI applications without the need for an external database?

I want to create a CLI application and I think this question is not about a specific technology but for the sake of reproduction purposes I'm using Node with command-line-commands ( but I know there are plenty others, e.g. commander ).
Given the following sample code
#!/usr/bin/env node
'use strict';
const commandLineArgs = require('command-line-args');
const commandLineCommands = require('command-line-commands');
const commandLineUsage = require('command-line-usage');
let isRunning = false; // global state
let commandResult;
try {
commandResult = commandLineCommands([ 'start', 'info', 'help' ]);
} catch (error) {
console.error('Invalid command.');
process.exit(1);
}
if (commandResult.command === null || commandResult.command === 'help') {
const commandInfo = commandLineUsage([
{ header: 'start', content: 'Sets the value to true' },
{ header: 'info', content: 'Gets the current value' },
]);
console.log(commandInfo);
process.exit(0);
}
let options;
try {
options = commandLineArgs([], { argv: commandResult.argv });
} catch (error) {
console.error('Invalid argument.');
process.exit(1);
}
if (commandResult.command === 'start') {
isRunning = true;
} else if (commandResult.command === 'info') {
console.info({ isRunning });
}
The boolean isRunning indicates a shared state. Calling the start command sets its value to true. But calling the info command obviously starts a new process and prints a new variable isRunning with its initial falsy value.
What is the prefered technology to keep such state? Must the CLI use an external database ( e.g. local filesystem) or are there some ways to keep the information in memory until shutdown?
Generating my own file on the system and storing this variable to it feels like an overkill to me.
An old cross-platform hack is to open a known TCP port. The first process able to open the port will get the port. All other processes trying to open the port will get an EADDRINUSE error:
const net = require('net');
const s = net.createServer();
s.on('error',() => {
console.log('Program is already running!');
// handle what to do here
});
s.listen(5123,'127.0.0.1',() => {
console.log('OK');
// run your main function here
});
This works in any language on any OS. There is only one thing you need to be careful of - some other program may be accidentally using the port you are using.
I originally came across this technique on the Tcl wiki: https://wiki.tcl-lang.org/page/singleton+application.
Another old hack for this is to try and create a symlink.
Creating symlinks are generally guaranteed to be atomic by most Unix and Unix-like OSes. Therefore there is no issue with potential race conditions using this technique (unlike creating a regular file). I presume it is also atomic on Windows (as per POSIX spec) but I'm not entirely sure:
const fs = require('fs');
const scriptName = process.argv[1];
const lockFile = '/tmp/my-program.lock';
try {
fs.symlinkSync(lockFile, scriptName);
// run your main function here
fs.unlinkSync(lockFile);
}
catch (err) {
console.log('Program already running');
// handle what to do here
}
Note: While creating symlinks are atomic, other operations on symlinks are not guaranteed to be atomic. Specifically be very careful of assuming that updating a symlink is atomic - it is NOT. Updating symlinks involve two operations: deleting the link and then creating the link. A second process may execute its delete operation after your process creates a symlink causing two processes to think that they're the only ones running. In the example above we delete the link after creating it, not before.
One way would be to use a local web server.
index.js
const commandLineArgs = require('command-line-args');
const commandLineCommands = require('command-line-commands');
const commandLineUsage = require('command-line-usage');
var http = require('http');
let globalState = {
isRunning: false
}
let commandResult;
try {
commandResult = commandLineCommands([ 'start', 'info', 'help' ]);
} catch (error) {
console.error('Invalid command.');
process.exit(1);
}
if (commandResult.command === null || commandResult.command === 'help') {
const commandInfo = commandLineUsage([
{ header: 'start', content: 'Sets the value to true' },
{ header: 'info', content: 'Gets the current value' },
]);
console.log(commandInfo);
process.exit(0);
}
let options;
try {
options = commandLineArgs([], { argv: commandResult.argv });
} catch (error) {
console.error('Invalid argument.');
process.exit(1);
}
if (commandResult.command === 'start') {
globalState.isRunning = true;
http.createServer(function (req, res) {
res.write(JSON.stringify(globalState));
res.end();
}).listen(9615);
} else if (commandResult.command === 'info') {
console.info({ globalState });
}
index2.js
var http = require('http');
var req = http.request({ host: "localhost", port: 9615, path: "/" }, (response) => {
var responseData = "";
response.on("data", (chunk) => {
responseData += chunk;
});
response.on("end", () => {
console.log(JSON.parse(responseData));
});
});
req.end();
req.on("error", (e) => {
console.error(e);
});
Here the index.js is a program that holds the "shared / global state" as well as creates a web server to communicate with. Other programs such as index2.js here can make a http request and ask for the global state. You could also let other programs change the state by having index.js listen to some specific request and act accordingly.
This doesn't have to be done with http like this, you could also use something like node-rpc or node-ipc. I thought the easiest working example would be to do it with a local http client and server.
Either way, I think the word for what you are looking for is Inter Process Communication (IPC) or Remote Procedure Call (RPC). I don't see why one couldn't also utilize websockets as well. Child processes probably won't work here, even if you could implement some kind of parent-child process communication, because only the child processes spawned by the main process could use that.
EDIT
After reading your question more carefully, I think that this is just a matter of "keeping" the "console session" after start command and setting the isRunning variable.
Check this out:
const commandLineArgs = require('command-line-args');
const commandLineCommands = require('command-line-commands');
const commandLineUsage = require('command-line-usage');
const prompt = require('prompt-sync')();
let globalState = {
isRunning: false
}
let commandResult;
try {
commandResult = commandLineCommands([ 'start', 'info', 'help' ]);
} catch (error) {
console.error('Invalid command.');
process.exit(1);
}
if (commandResult.command === null || commandResult.command === 'help') {
const commandInfo = commandLineUsage([
{ header: 'start', content: 'Sets the value to true' },
{ header: 'info', content: 'Gets the current value' },
]);
console.log(commandInfo);
process.exit(0);
}
let options;
try {
options = commandLineArgs([], { argv: commandResult.argv });
} catch (error) {
console.error('Invalid argument.');
process.exit(1);
}
if (commandResult.command === 'start') {
globalState.isRunning = true;
while(globalState.isRunning)
{
let cmd = prompt(">");
if(cmd === "exit")
process.exit(0);
if(cmd === "info")
console.info({ globalState });
}
} else if (commandResult.command === 'info') {
console.info({ globalState });
}
Here I am using prompt-sync library inside a loop when the program is called with a start command. The "console session" is kept indefinitely until the user types exit. I also added and example for in case the user types info.
Example:

How to solve timeout problem with electron-html-to node.js

I'm experiencing this timeout when trying to use electron to convert an html file to pdf. I'm running this js app through node.
`{ Error: Worker Timeout, the worker process does not respond after 10000 ms
at Timeout._onTimeout (C:\Users\Owner\Desktop\code\PDF-Profile-Generator\node_modules\electron-workers\lib\ElectronManager.js:377:21)
at ontimeout (timers.js:436:11)
at tryOnTimeout (timers.js:300:5)
at listOnTimeout (timers.js:263:5)
at Timer.processTimers (timers.js:223:10)
workerTimeout: true,
message:
Worker Timeout, the worker process does not respond after 10000 ms,
electronTimeout: true }`
I do not know too much about electron. I have not been able to try too much to try to debug it. The js code is meant to generate an html file based on user input, pulling from a github profile. Then, that html file needs to be converted to a pdf file.
My js code is as follows:
const fs = require("fs")
const convertapi = require('convertapi')('tTi0uXTS08ennqBS');
const path = require("path");
const generate = require("./generateHTML");
const inquirer = require("inquirer");
const axios = require("axios");
const questions = ["What is your Github user name?", "Pick your favorite color?"];
function writeToFile(fileName, data) {
return fs.writeFileSync(path.join(process.cwd(), fileName), data);
};
function promptUser() {
return inquirer.prompt([
{
type: "input",
name: "username",
message: questions[0]
},
{
type: "list",
name: "colorchoice",
choices: ["green", "blue", "pink", "red"],
message: questions[1]
}
])
};
function init() {
promptUser()
.then(function ({ username, colorchoice }) {
const color = colorchoice;
const queryUrl = `https://api.github.com/users/${username}`;
let html;
axios
.get(queryUrl)
.then(function (res) {
res.data.color = color
const starArray = res.data.starred_url.split(",")
res.data.stars = starArray.length
console.log(res)
html = generate(res.data);
console.log(html)
writeToFile("profile.html", html)
})
var convertFactory = require('electron-html-to');
var conversion = convertFactory({
converterPath: convertFactory.converters.PDF
});
conversion({ file: './profile.html' }, function (err, result) {
if (err) {
return console.error(err);
}
console.log(result.numberOfPages);
console.log(result.logs);
result.stream.pipe(fs.createWriteStream(__dirname + '/profile.pdf'));
conversion.kill(); // necessary if you use the electron-server strategy, see bellow for details
});
// convertapi.convert('pdf', { File: './profile.html' })
// .then(function (result) {
// // get converted file url
// console.log("Converted file url: " + result.file.url);
// // save to file
// return result.file.save(__dirname + "/profile.pdf");
// })
// .then(function (file) {
// console.log("File saved: " + file);
// });
})
}
init();
I had a similar problem. I had installed multiple versions of Electron (electron, electron-html-to, electron-prebuilt), and the problem was resolved when I deleted the older versions in package.json so only one was left. The assumption is that they were interfering with each other.
So check the installed versions of electron, because the problem might be there rather than your code.

Data stream handlers and CLI incorporation Node JS

I am trying to create an interactive CLI that can run serial commands. I have two files serialcomms.js and cli.js. Serialcomms.js contains the connection, handlers, and command functions. cli.js contains the commander information.
My issue is that I can only call the send command once because the listeners/handlers take over from the serialcomms file. What would be the best method to loop the cli program so I can call the send command over and over again, but still have the serial handlers running and output to stdout? Would I need to use a child process? Or recursion to have the cli call itself?
Example behavior I am expecting with an echo bot on the other end of the serial line.
Send hello
hello
Send Bye
Bye
Behavior I am experiencing
Send hello
hello
endless wait
Here is my serialcomms.js
const SerialPort = require('serialport');
const ReadLine = require('#serialport/parser-readline');
let portName = `/dev/pts/${process.argv[2]}` || '/dev/pts/6';
let baudRate = process.argv[3] || 115200;
let myPort = new SerialPort(portName, {baudRate: baudRate})
let parser = myPort.pipe(new ReadLine({ delimiter: '\n' }))
myPort.on('open', () => {
console.log(`port ${portName} open`)
})
parser.on('data', (data) => {
console.log(data)
})
myPort.on('close', () => {
console.log(`port ${portName} closed`)
})
myPort.on('error', (err) => {
console.error('port error: ' + err)
})
function send(data){
myPort.write(JSON.stringify(data)+'\n', function(err) {
if (err) {
return console.log('Error on write: ', err.message);
}
console.log(`${data} sent`);
});
}
module.exports = {
send
}
Here is my CLI.js file
const program = require('commander');
const {send} = require('./serialcomms');
program
.version('1.0.0')
.description('Serial Tester')
program
.command('send <msg>')
.alias('s')
.description('send a message over serial')
.action((msg)=>{
send(msg)
})
program.parse(process.argv)

Categories

Resources