This content originally appeared on Level Up Coding - Medium and was authored by Itsuki
Do you have a Node.JS App where you need to run a specific command line command to start another server/service, and all the other processing you have need that service to be running?
I do!
I originally thought all I have to do is to exec(“node index.js”) or whatever command you have to start your service, but it is actually a little bit more than that due to the following reasons.
- The port that the service needs to listen to might already be in use
- The server/service normally does not start immediately right after the command. There are set ups that needs to be done before it actually starts listening on a port
- We need the service to be running while doing other processing
So! Here is what we will be doing in this article. Let’s see how we can use Child process to
- Check if the port is in use
- Kill the port if it is
- Send the command to start the service/server
- Wait for the set up to finish until we move onto executing other code
- Stop the service when we are done!
Ready, set, go!
Set Up
Let’s create a simple CommandLineService class to help us manage all the logic!
import { ChildProcess, spawn } from 'child_process';
export class CommandLineService {}
Entirely empty right now! Adding more as we move on!
Check Port
I am running mine on Windows, so to check whether if a port is in use and get the PID of it, I will be using the netstat command. If you are using Mac or Linux, you would probably want to use lsof instead.
// ...
async getPids(port: number): Promise<string[]> {
var resolver: (value: string[]) => void, promise = new Promise<string[]>(function (resolve) {
resolver = resolve;
});
const findstr = spawn('findstr', [":".concat(`${port}`, ".*")], {
stdio: ['pipe'],
});
const netstat = spawn('netstat', ['-ano'], {
stdio: ['ignore', findstr.stdin],
});
var result = '';
findstr.stdout.on('data', function (data) { return (result += data); });
findstr.on('close', function () {
const pids = result.trim().match(/\d+$/gm);
const uniquePids = pids?.filter((value: string, index: number, array: string[]) => {
return array.indexOf(value) === index;
})
console.log(`Pids for port ${port}: ${uniquePids}`);
return resolver(uniquePids ?? []);
});
findstr.stdin.end();
return promise;
}
Note that we cannot run netstat -ano | findstr :{port} directly like we do from PowerShell. And that’s why instead of simply use execSync, we are using spawn + promise here so that we can define the stdio of netstat to be that of findstr and return the result on close.
To use it
const pids = await this.getPids(25);
Kill Process by PID
Again, I am running on Windows, so we will use taskkill here. Change it to kill if you are on Mac/Linux.
Fortunately, nothing special this time! We can just simply use execSync and wait for it to finish!
killPids(pids: string[]) {
for (let index in pids) {
this.killPid(pids[index])
}
}
killPid(pid: string) {
const output = execSync(`taskkill /PID ${pid} /F`, {
encoding: 'utf-8'
});
console.log('PID killed: ', pid);
console.log('Output: ', output);
}
And use it
this.killPids(pids)
Start the Service
Now that we have our port freed up, we can start our service!
We will be using the spawn command here. Different from exec, it is more suitable for long-running process (which is exactly what we are looking for)!
I will be using a Mail server that is started by running an executable as my example here!
private subprocess: ChildProcess | null = null;
async startMailServer(): Promise<void> {
const exePath = process.env.MAIL_SERVER_EXE_PATH;
var resolver: (value: void) => void, promise = new Promise<void>(function (resolve) {
resolver = resolve;
});
// mail server port
const mailPortPids = await this.getPids(25);
this.killPids(mailPortPids)
const subprocess = spawn(exePath, {
detached: true,
});
subprocess.stdout?.on('data', (data) => {
console.log(`stdout: ${data}`);
if (`${data}`.includes(`Now listening on: http://localhost:25`)) {
return resolver();
}
});
subprocess.stderr?.on('data', (data) => {
console.error(`stderr: ${data}`);
});
subprocess.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
this.subprocess = subprocess;
return promise;
}
Couple important points!
We are using resolver and promise here because our server will probably need some time to set up after the command and actually start listening on port. I am checking by matching the string Now listening on: http://localhost:25 but you should change that to whatever stdout you are expecting to get when your service is actually started.
We cannot return resolver on close because we need our subprocess to keep running and it SHOULD NOT and CANNOT close until we kill it!
For the same reason, we have specified detached: true here. Do note that this option performs a little differently between windows and other platforms.
- On Windows, setting options.detached to true makes it possible for the child process to continue running after the parent exits. The child will have its own console window. Once enabled for a child process, it cannot be disabled.
- On non-Windows platforms, if options.detached is set to true, the child process will be made the leader of a new process group and session. Child processes may continue running after the parent exits regardless of whether they are detached or not. See setsid(2)for more information.
And since we need to come back and kill our subprocess later when our Main Node.Js App finishes running, I am keeping a reference to it within the class.
Stop the service
To finish our day, we will have to stop the spawned service/server when we finish. It can simply be achieved by calling kill on the subprocess we created above.
stopMailServer() {
const killed = this.subprocess?.kill();
console.log(`Mail server stop with success: ${killed} `);
}
The kill function returns true if kill(2) succeeds, and false otherwise.
Wrap Up
Just to wrap what we have above together, here we go! Our CommandLineService class.
import { ChildProcess, spawn } from 'child_process';
export class CommandLineService {
private subprocess: ChildProcess | null = null;
async startMailServer(): Promise<void> {
const exePath = 'server.exe';
var resolver: (value: void) => void, promise = new Promise<void>(function (resolve) {
resolver = resolve;
});
// mail server port
const mailPortPids = await this.getPids(25);
this.killPids(mailPortPids)
const subprocess = spawn(exePath, {
detached: true,
});
subprocess.stdout?.on('data', (data) => {
console.log(`stdout: ${data}`);
if (`${data}`.includes(`Now listening on: http://localhost:25`)) {
return resolver();
}
});
subprocess.stderr?.on('data', (data) => {
console.error(`stderr: ${data}`);
});
subprocess.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
this.subprocess = subprocess;
return promise;
}
stopMailServer() {
const killed = this.subprocess?.kill();
console.log(`Mail server stop with success: ${killed} `);
}
killPids(pids: string[]) {
for (let index in pids) {
this.killPid(pids[index])
}
}
killPid(pid: string) {
const output = execSync(`taskkill /PID ${pid} /F`, {
encoding: 'utf-8'
});
console.log('PID killed: ', pid);
console.log('Output: ', output);
}
async getPids(port: number): Promise<string[]> {
var resolver: (value: string[]) => void, promise = new Promise<string[]>(function (resolve) {
resolver = resolve;
});
const findstr = spawn('findstr', [":".concat(`${port}`, ".*")], {
stdio: ['pipe'],
});
const netstat = spawn('netstat', ['-ano'], {
stdio: ['ignore', findstr.stdin],
});
var result = '';
findstr.stdout.on('data', function (data) { return (result += data); });
findstr.on('close', function () {
const pids = result.trim().match(/\d+$/gm);
const uniquePids = pids?.filter((value: string, index: number, array: string[]) => {
return array.indexOf(value) === index;
})
console.log(`Pids for port ${port}: ${uniquePids}`);
return resolver(uniquePids ?? []);
});
findstr.stdin.end();
return promise;
}
}
We can then simply call it from other parts of our code like following.
const service = new CommandLineService();
// to start server/service
await service.startMailServer();
// to stop
service.stopMailServer();
Thank you for reading!
That’s all I have for today!
Happy spawning! Like a Zombie!
Node.JS/Typescript: Use Child Process to Start/Stop Another Service was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Itsuki
Itsuki | Sciencx (2024-07-23T15:38:42+00:00) Node.JS/Typescript: Use Child Process to Start/Stop Another Service. Retrieved from https://www.scien.cx/2024/07/23/node-js-typescript-use-child-process-to-start-stop-another-service/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.