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.

3

There are 3 best solutions below

6
On

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:

example output

1
On

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.

10
On

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.