Comparing version 0.1.0 to 0.2.0
170
bin/deity.js
#!/usr/bin/env node | ||
var cp = require("child_process"); | ||
var net = require("net"); | ||
require("colors"); | ||
// Deity is a command-line application to make it easier to manage and manipulate processes. | ||
// Its star use case is running an interacting with compilers or pre-processors such as SCSS or Coffeescript, without having to run separate shells for each process. | ||
// `deity.js` is the frontend to [deityd.js](deityd.html), the daemon that actually spawns and looks after processes. | ||
// The frontend and daemon communicate though a TCP socket on the following port: | ||
var PORT = 51145; | ||
var HOST = "localhost"; | ||
var cp = require("child_process"); | ||
var net = require("net"); | ||
// Communication with deityd | ||
// --------------------------- | ||
// `deity.js` sends messages to the daemon in the form of stringified JSON. | ||
// This JSON generally contains 3 fields: | ||
// | ||
// - `operation`: Operation to perform (new, kill, list, etc.); | ||
// - `name`: Name of the process, as `deityd` identifies them, to work on; | ||
// - `message`: Any additional information to pass to `deityd`. | ||
// | ||
// Note that the `processName` and `message` fields aren't always applicable for certain operations (like `list`). | ||
// | ||
// The TCP message is sent in the function `send()`. | ||
// The JSON object is made in the function `constructJSON()`. | ||
var INFO_MSG = [ | ||
'Deity by Arthanzel', | ||
'== Usage', | ||
'node deity.js <command> [arguments]', | ||
'== Commands', | ||
'new <name> <command>: Creates a new process with a name and command to run', | ||
'list: lists all running processes', | ||
'kill <name>: kills a running process', | ||
'force: force exits the deity daemon' | ||
].join("\n"); | ||
var ALLOWED_OPS = ["new", "kill", "list", "out", "in", "exit", "force"]; | ||
// Print the help message if no command is given | ||
if (process.argv[2] == null) { | ||
console.log(INFO_MSG); | ||
process.exit(); | ||
// If the user specified an operation that's not allowed, print the help and exit. | ||
// Don't even bother pinging `deityd` since it'll do absolutely nothing. | ||
// This will also run if the user didn't type an operation (`argv[2]` is null). | ||
if (ALLOWED_OPS.indexOf(process.argv[2]) == -1) { | ||
printHelp(); | ||
} | ||
// Make sure that deityd is running. | ||
// Sending messages | ||
// ---------------- | ||
// Make sure that deityd is running before trying to send it commands. | ||
ensureDaemon(); | ||
// Ensures that deityd is running by trying to connect to its socket. | ||
// If no connection can be established, deityd is started. | ||
// Ensure that `deityd` is running by connecting to its TCP socket. | ||
// If `deityd` is not running, this function will start €it. | ||
function ensureDaemon() { | ||
var socket = new net.Socket(); | ||
// 200 ms is more than enough time for `deityd` to respond to a TCP connection originating from the same host. | ||
// At the same time, we don't want this too short in case the OS is tasked to capacity and `deityd` doesn't repond immediately, but if it's too long then it will hinder Deity's usability. | ||
// In any case, the socket should not be timing out under normal conditions. | ||
// If the socket connects or fails, it will generally do this well before the 200 ms mark. | ||
socket.setTimeout(200); | ||
// Socket connected and deityd is running. | ||
// If the socket connects, `deityd` is running. | ||
// Kill the test socket and start sending messages to `deityd` over TCP. | ||
socket.on("connect", function() { | ||
@@ -40,8 +61,9 @@ socket.end(); | ||
//console.log("Deity daemon is running"); | ||
start(); | ||
send(); | ||
}); | ||
// Socket failed to connect and deityd is not running. | ||
// If the socket fails, `deityd` is not running. | ||
// Note that this case is NOT the 200 ms timeout. | ||
// A failing socket returns immediately. | ||
// In this case, start `deityd` and send it some messages.€ | ||
socket.on("error", function() { | ||
@@ -52,9 +74,10 @@ socket.end(); | ||
startDaemon(); | ||
console.log("Starting deity daemon..."); | ||
console.log("Starting deity daemon...".grey); | ||
// Give the daemon some time to start before sending it commands. | ||
setTimeout(start, 200); | ||
setTimeout(send, 200); | ||
}); | ||
// Socket timed out. Can't determine if deityd is running, so notify and exit. | ||
// Socket timed out. | ||
// Can't determine if deityd is running, so notify and exit. | ||
socket.on("timeout", function() { | ||
@@ -64,24 +87,26 @@ socket.end(); | ||
console.error("Can't determine if deity is running"); | ||
var address = HOST + ":" + PORT; | ||
console.error("Can't determine if deity is running."); | ||
console.error("Try killing all node processes and trying again."); | ||
console.error("You may need to open port localhost:51145.") | ||
console.error("You may need to open port " + address + "."); | ||
}); | ||
// Try pinging `deityd`. | ||
// Remember, the above functions are events, so this line will execute first. | ||
socket.connect(PORT); | ||
} | ||
// Consolidates any extra arguments into a single string. | ||
function getArguments() { | ||
return process.argv.slice(3).join(" "); | ||
} | ||
// Sends a message to deityd. | ||
function send(command, message) { | ||
// Sends a message to `deityd`. | ||
function send() { | ||
var socket = new net.Socket(); | ||
// Print any data received from the daemon | ||
// Print any data received from the daemon. | ||
socket.on("data", function(buffer) { | ||
// The buffer will contain a newline at the end, so `trim()` it out before printing. | ||
console.log(buffer.toString().trim()); | ||
}); | ||
// Exit the script if deityd terminates the TCP connection. | ||
// Exit `deity` if `deityd` terminates the TCP connection. | ||
// This happens when an operation has (successfully) completed. | ||
// The socket should always be ended by `deityd`. | ||
socket.on("end", function() { | ||
@@ -91,34 +116,57 @@ process.exit(); | ||
// In the case that `deityd` has encountered an error and does not end the socket within 5 seconds, | ||
// print a message and end the socket. | ||
// This will cause this script to exit, as per the above "end" event. | ||
setTimeout(function() { | ||
console.error("ERROR: Deity daemon has stopped responding!") | ||
console.error(" Will try to exit, but your processes may still be running."); | ||
socket.end(); | ||
}, 5000); | ||
socket.connect(PORT); | ||
socket.write(command + message); | ||
// Socket will be ended by deityd | ||
socket.write(constructJSON()); | ||
} | ||
// Processes arguments and sends the appropriate message to deityd. | ||
function start() { | ||
var command = process.argv[2]; | ||
// Constructs a JSON object to send to `deityd` from the process's arguments. | ||
function constructJSON() { | ||
return JSON.stringify ({ | ||
operation: process.argv[2], | ||
name: process.argv[3] || "", | ||
message: process.argv.slice(4).join(" ") | ||
}); | ||
} | ||
if (command == "exit") { | ||
send("x", ""); | ||
} | ||
else if (command == "force") { | ||
send ("!", ""); | ||
} | ||
else if (command == "list") { | ||
send("l", ""); | ||
} | ||
else if (command == "kill") { | ||
send("k", getArguments()); | ||
} | ||
else if (command == "new") { | ||
send("n", getArguments()); | ||
} | ||
// Prints the help message to the console and exits. | ||
function printHelp() { | ||
var HELP_MSG = [ | ||
"Usage: deity <operation> [arguments]", | ||
"Operations:", | ||
" new <name> <command> Spawns a new process called <name> by running <command>.", | ||
" kill <name> Kills a running process in Deity called <name>", | ||
" out <name> Prints the stdout/stderr for process <name>.", | ||
" in <name> <message> Sends <message> to the stdin of process <name>", | ||
" list Lists running processes with their PID. Also indicates if", | ||
" the process has unread lines in stdout or stderr.", | ||
" exit Nicely quits all Deity processes and exits the Deity daemon.", | ||
" force Force-quits the Deity daemon. Use with caution." | ||
].join("\n"); | ||
console.log(HELP_MSG); | ||
process.exit(); | ||
} | ||
// Start deityd | ||
// Daemon-starting | ||
// --------------- | ||
function startDaemon() { | ||
var deityd = cp.spawn("deityd", [], { stdio: "ignore", detached: true }); | ||
// These options will allow `deityd` to run as a daemon even when this script exits. | ||
// Setting the `detached` flag isn't enough; by default, subprocesses share their parent's standard input and output. | ||
// When the parent process exits, it closes its stdio and so the subprocess will exit as well. | ||
// Node provides a shortcut `stdio: "ignore"` to route the subprocess's stdio to null, so that it's not dependent on the parent's io streams. | ||
var opts = { stdio: 'ignore', detached: true }; | ||
// Prevent the current script from waiting until deityd exits. | ||
// We don't need to pass any arguments to `deityd`. | ||
var deityd = cp.spawn("deityd", [], opts); | ||
// Prevent the current script from waiting until deityd exits by taking it out of the event loop. | ||
deityd.unref(); | ||
} |
#!/usr/bin/env node | ||
var cp = require("child_process"); | ||
var fs = require("fs"); | ||
var net = require("net"); | ||
var moment = require("moment"); | ||
require("colors"); | ||
// `deityd` is a daemon that runs as long as there are running processes managed by deity. | ||
// Daemonizing allows Deity to retain handles to processes, standard input streams, and events, which would otherwise be impossible if it tracked PIDs. | ||
// Additionally, PIDs aren't very well supported on all systems, even through Node.js. | ||
// You might see some calls to `console.log`, but `deityd` is always started with its stdio routed to null. | ||
// Although it can log output, it doesn't go anywhere unless told otherwise. | ||
// `deityd` receives commands through a TCP socket running on the following port: | ||
var PORT = 51145; | ||
var HOST = "localhost"; | ||
var cp = require("child_process"); | ||
var dgram = require("dgram"); | ||
var net = require("net"); | ||
// Messages are received as stringified JSON. | ||
// For an explanation of the schema, see [deity.js](deity.html). | ||
// This hash holds all of the running processes. | ||
// The key corresponds to the name of the process as given to Deity, and values contain process objects created when Node spawns a child process, plus some extra properties that Deity adds. | ||
var processes = {}; | ||
startDaemon(); | ||
// Serving requests | ||
// ---------------- | ||
startServer(); | ||
// Returns the number of running processes | ||
function countProcesses() { | ||
return Object.getOwnPropertyNames(processes).length; | ||
} | ||
// Here, `deityd` starts a TCP server that lives as long as the daemon runs. | ||
// This TCP serve accepts connections, parses the JSON object and delegates work to the appropriate functions. | ||
function startServer() { | ||
// The function is fired when a client connects. | ||
var server = net.createServer(function(socket) { | ||
/** | ||
* Close all processes and exit deityd. | ||
*/ | ||
function exit(socket, force) { | ||
// Close each process. | ||
for (var p in processes) { | ||
processes[p].kill(); | ||
} | ||
// Event fired when a command has been received. | ||
socket.on("data", function(buffer) { | ||
json = JSON.parse(buffer.toString()); | ||
if (force) | ||
// Delegate each operation to the appropriate method. | ||
switch (json.operation || "list") { | ||
case "new": | ||
if (!json.name || !json.message) { | ||
socket.end("Usage: deity new <name> <command>") | ||
break; | ||
} | ||
var msg = newProcess(json.name, json.message); | ||
socket.end(msg); | ||
break; | ||
case "kill": | ||
if (!json.name) { | ||
socket.end("Usage: deity kill <name>") | ||
break; | ||
} | ||
msg = killProcess(json.name); | ||
socket.end(msg); | ||
break; | ||
case "list": | ||
msg = listProcesses(); | ||
socket.end(msg); | ||
break; | ||
case "out": | ||
if (!json.name) { | ||
socket.end("Usage: deity out <name>") | ||
break; | ||
} | ||
msg = listProcessOut(json.name); | ||
socket.end(msg); | ||
break; | ||
case "in": | ||
if (!json.name || !json.message) { | ||
socket.end("Usage: deity in <name> <message>") | ||
break; | ||
} | ||
msg = writeProcessIn(json.name, json.message); | ||
socket.end(msg); | ||
break; | ||
case "exit": | ||
socket.write("Stopping deity daemon..."); | ||
exitDaemon(); | ||
break; | ||
case "force": | ||
socket.end("Force exiting!"); | ||
process.exit(); | ||
break; | ||
default: | ||
// In case of fire, inform user. | ||
socket.end("Don't know how to run " + json.operation); | ||
break; | ||
} | ||
// Exit if no processes are running. | ||
exitIfIdle(); | ||
}); | ||
}); | ||
// Notify and exit if the server failed to start, probably because the port is blocked. | ||
server.on("error", function() { | ||
console.log("Failed to start. Is deityd already running?"); | ||
process.exit(); | ||
else | ||
exitLoop(socket); | ||
}); | ||
// Run the server. | ||
// Will fail if the port is already bound, ensuring a single instance of the daemon. | ||
server.listen(PORT, function() { | ||
console.log("Server listening"); | ||
}); | ||
} | ||
// Try exiting every 500 ms if every process has died. | ||
// TODO: Implement a process counter using events and exit deityd when all processes are closed. | ||
function exitLoop(socket) { | ||
if (countProcesses() == 0) { | ||
socket.end(); | ||
process.exit(); | ||
// Performing operations | ||
// --------------------- | ||
// Spawns a new process called `name` by running `command`. | ||
function newProcess(name, command) { | ||
// First, check if a process by the given name already eixsts. | ||
// If so, display a message to the user and return. | ||
if (processes[name] != null) { | ||
return "Process '" + name + "' already exists." | ||
} | ||
else { | ||
setTimeout(function() { exitLoop(socket); }, 500); | ||
} | ||
// Run the process and save it to the `processes` map. | ||
var p = cp.exec(command); | ||
processes[name] = p; | ||
p.cmd = command; | ||
// Save standard error and output into an array in the process object for easy manipulation. | ||
p.outLines = []; | ||
p.stdout.on("data", function(data) { | ||
var time = moment(new Date()).format("HH:mm:ss"); | ||
p.outLines.push(time + " :: " + data.trim()); | ||
}); | ||
p.stderr.on("data", function(data) { | ||
var time = moment(new Date()).format("HH:mm:ss"); | ||
p.outLines.push(time + " :: " + data.trim().red); | ||
}); | ||
// To allow `deityd` to show if processes have printed to the stdout/err, and highlight old lines of output, | ||
// keep track of how many output lines the user has seen. | ||
p.outLinesRead = 0; | ||
// As soon as a process exits, remove it from the `processes` map. | ||
p.on("exit", function() { | ||
delete processes[name]; | ||
// Exit `deityd` if the last process has been killed. | ||
exitIfIdle(); | ||
}); | ||
// This will be the string that is sent back to `deity.js` through the socket. | ||
return "New process " + name + "(" + p.pid + ")"; | ||
} | ||
// Kills the named processes. | ||
function killProcess(p) { | ||
if (processes[p] != null) { | ||
processes[p].kill(); | ||
// Kills the named process. | ||
// This function does **not** wait for the process to be killed to return. If the process doesn't die, it will still show up when `deity list` is run. | ||
// Possibly, processes may be waiting for user intervention before quitting, but those kinds of processes are generally not the ones you want to run using `diety`. | ||
function killProcess(name) { | ||
if (processes[name] != null) { | ||
processes[name].kill(); | ||
return "Killing " + name; | ||
} | ||
@@ -56,7 +171,6 @@ else { | ||
// Lists all running processes. | ||
function list() { | ||
// Lists all running processes started by `deity`. | ||
function listProcesses() { | ||
var n = countProcesses(); | ||
// Check if the processes map is empty | ||
if (n == 0) { | ||
@@ -66,14 +180,20 @@ return "No processes"; | ||
else { | ||
var output = "" + n + " running "; | ||
// This will print "n process(es)" with correct grammar. | ||
var output = n.toString() + " process"; | ||
if (n > 1) output += "es"; | ||
output += "\r\n"; | ||
// Pluralize | ||
if (n == 1) | ||
output += "process \r\n"; | ||
else | ||
output += "processes \r\n"; | ||
// List each process and add it to the output | ||
for (var name in processes) { | ||
p = processes[name]; | ||
output += name; | ||
output += "(" + p.pid + ")"; | ||
// List each process | ||
for (var p in processes) { | ||
output += p; | ||
output += "(" + processes[p].pid + ")"; | ||
// If the process has printed to stdout/error, list the number of new lines. | ||
var difference = p.outLines.length - p.outLinesRead; | ||
if (difference > 0) { | ||
var str = " " + difference + " new messages"; | ||
output += str.yellow; | ||
} | ||
output += "\r\n"; | ||
@@ -86,72 +206,74 @@ } | ||
// Spawns a new process | ||
function newProcess(message, socket) { | ||
console.log("New process: " + message); | ||
// Show the standard error and output for a process. | ||
// Each log to the stdout/err will be on a separate line and timestamped. | ||
// Lines with linebreaks in them will be indented to maintain consistency. | ||
function listProcessOut(name) { | ||
if (processes[name] == null) { | ||
msg += "Process '" + name + "' doesn't exist."; | ||
return msg; | ||
} | ||
// Separate the process name from the command | ||
var processName = message.split(" ", 1)[0]; | ||
var processCommand = message.substring(processName.length + 1); | ||
var p = processes[name]; | ||
var output = ""; | ||
// Run the process and save it to the list | ||
var process = cp.exec(processCommand); | ||
processes[processName] = process; | ||
processes[processName].cmd = processCommand; | ||
for (var i = 0; i < p.outLines.length; i++) { | ||
var lines = p.outLines[i].split("\n"); | ||
// Remove an exited process from the list | ||
process.on("exit", function() { | ||
delete processes[processName]; | ||
}); | ||
// In case the line in the output contains linebreaks, gracefully split it into multiple lines and indent them all to the same level. | ||
for (var j = 0; j < lines.length; j++) { | ||
if (j > 0) { | ||
output += " "; | ||
// "HH:mm:ss :: " | ||
} | ||
return "New process " + processName + "(" + process.pid + ")"; | ||
// If the line was already read, grey it out. | ||
// Note that this will not stop read errors from appearing red. | ||
// Their timestamps, however, will be grey. | ||
if (p.outLinesRead > i) | ||
output += lines[j].grey; | ||
else | ||
output += lines[j]; | ||
output += "\n"; | ||
} | ||
} | ||
p.outLinesRead = p.outLines.length; | ||
return output; | ||
} | ||
function startDaemon() { | ||
// TCP server on the daemon. | ||
// The function is fired on a connection. | ||
var server = net.createServer(function(socket) { | ||
// Event fired when a command has been received. | ||
socket.on("data", function(buffer) { | ||
data = buffer.toString().trim(); | ||
function writeProcessIn(name, message) { | ||
if (processes[name] == null) { | ||
msg += "Process '" + name + "' doesn't exist."; | ||
return msg; | ||
} | ||
var command = data.toString().charAt(0); | ||
var message = data.toString().substring(1); | ||
processes[name].stdin.write(message + "\n"); | ||
} | ||
if (command == "k") { // Kill | ||
killProcess(message, socket); | ||
socket.end("Killing " + message); | ||
} | ||
else if (command == "l") { // List | ||
socket.end(list()); | ||
} | ||
else if (command == "n") { // New | ||
socket.end(newProcess(message, socket)); | ||
} | ||
else if (command == "x") { // Exit | ||
// Kill the connection to the client. | ||
socket.write("Stopping deity daemon..."); | ||
exit(socket, false); | ||
} | ||
else if (command == "!") { // Force exit | ||
socket.end("Force exiting!") | ||
process.exit(socket, true); | ||
} | ||
}); | ||
// Attempts to kill all processes and exits `deityd`. | ||
function exitDaemon() { | ||
exitIfIdle(); | ||
for (var name in processes) { | ||
processes[name].kill(); | ||
} | ||
} | ||
// Notify when a connection dies. | ||
socket.on("end", function() { | ||
console.log("Disconnected"); | ||
}); | ||
}); | ||
// Notify and exit if the server failed to start, probably because the port is blocked. | ||
server.on("error", function() { | ||
console.log("Failed to start. Is deityd already running?"); | ||
// Automatically exit `deityd` when the last process exits. | ||
// There is no reason to keep `deityd` running if it's not doing anything, and it makes the entire process of starting and stopping processes transparent to the user without having to worry about starting/stopping the daemon. | ||
// This function is called when an operation completes and whenever a process exits. | ||
function exitIfIdle() { | ||
if (countProcesses() == 0) { | ||
process.exit(); | ||
}); | ||
} | ||
} | ||
// Run the server. | ||
// Will fail if the port is already bound, ensuring a single instance of the daemon. | ||
server.listen(PORT, function() { | ||
console.log("Server listening"); | ||
}); | ||
} | ||
// Returns the number of running processes. | ||
function countProcesses() { | ||
return Object.getOwnPropertyNames(processes).length; | ||
} | ||
{ | ||
"name": "deity-cli", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "Easy-peasy command-runner for big programming projects.", | ||
@@ -8,2 +8,6 @@ "scripts": { | ||
}, | ||
"dependencies": { | ||
"colors": "0.6.2", | ||
"moment": "2.7.0" | ||
}, | ||
"bin": { | ||
@@ -10,0 +14,0 @@ "deity": "./bin/deity.js", |
@@ -29,11 +29,13 @@ Book of Deity | ||
6. Thou shall kill Processes started by Deity by executing `deity kill GLaDOS` and the Process that shall be killed will be the Process whose name is GLaDOS. | ||
7. Thou shall exit the Daemon and all Processes started by Deity by executing `deity exit`. | ||
8. Thou shall force exit the Daemon by executing `deity force`. | ||
9. Thou shall not force exit needlessly, as it may leave zombie processes and memory leaks. | ||
10. Thou shall hold no creators of Diety above Arthanzel (respect the MIT license and don't mooch credit). | ||
11. **Thou shall not complain if Diety deletes your computer for Deity is a work in progress and you shall be ignored.** | ||
7. Thou shall get the standard input and error of process GlaDOS by executing `deity out GlaDOS`. | ||
8. Thou shall write "cake" to the standard input of process GlaDOS by executing `deity in GlaDOS cake`. | ||
9. Thou shall exit the Daemon and all Processes started by Deity by executing `deity exit`. | ||
10. Thou shall force exit the Daemon by executing `deity force`. | ||
11. Thou shall not force exit needlessly, as it may leave zombie processes and memory leaks. | ||
12. Thou shall hold no creators of Diety above Arthanzel (respect the MIT license and don't mooch credit). | ||
13. **Thou shall not complain if Diety deletes your computer for Deity is a work in progress and you shall be ignored.** | ||
Roadmap | ||
======= | ||
- Run checks whether a process exists already exists before starting a new one. | ||
- "in" and "out commands" to manipulate the stdio and stdout. | ||
Pages todo | ||
========== | ||
- Github badge | ||
- Dark grey on light grey? |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
438133
23
1157
41
2
1
+ Addedcolors@0.6.2
+ Addedmoment@2.7.0
+ Addedcolors@0.6.2(transitive)
+ Addedmoment@2.7.0(transitive)