Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a service socket #603

Merged
merged 3 commits into from
Jul 22, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,9 @@ setuid:
# concurrent updates), then run `node lib/channel-storage/migrate.js`.
channel-storage:
type: 'file'

# Allows for external services to access the system commandline
# Useful for setups where stdin isn't available such as when using PM2
service-socket:
enabled: false
socket: 'service.sock'
42 changes: 42 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ process.stdin.on("data", function (data) {
}
});

var validIP = require('net').isIP;
function handleLine(line) {
if (line === "/reload") {
Logger.syslog.log("Reloading config");
Expand Down Expand Up @@ -75,5 +76,46 @@ function handleLine(line) {
}
} else if (line.indexOf("/reload-partitions") === 0) {
sv.reloadPartitionMap();
} else if (line.indexOf("/globalban") === 0) {
var args = line.split(/\s+/); args.shift();
if (args.length >= 2 && validIP(args[0]) !== 0) {
var ip = args.shift();
var comment = args.join(' ');
require("./lib/database").globalBanIP(ip, comment, function (err, res) {
if (!err) {
Logger.eventlog.log("[acp] " + "SYSTEM" + " global banned " + ip);
}
})
}
} else if (line.indexOf("/unglobalban") === 0) {
var args = line.split(/\s+/); args.shift();
if (args.length >= 1 && validIP(args[0]) !== 0) {
var ip = args.shift();
require("./lib/database").globalUnbanIP(ip, function (err, res) {
if (!err) {
Logger.eventlog.log("[acp] " + "SYSTEM" + " un-global banned " + ip);
}
})
}
} else if (line.indexOf("/unloadchan") === 0) {
var args = line.split(/\s+/); args.shift();
if(args.length){
var name = args.shift();
var chan = sv.getChannel(name);
var users = Array.prototype.slice.call(chan.users);
chan.emit("empty");
users.forEach(function (u) {
u.kick("Channel shutting down");
});
Logger.eventlog.log("[acp] " + "SYSTEM" + " forced unload of " + name);
}
}
}

// Go Go Gadget Service Socket
if (Config.get("service-socket.enabled")) {
Logger.syslog.log("Opening service socket");
var ServiceSocket = require('./lib/servsock');
var server = new ServiceSocket;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this even legal in ECMAScript?

server.init(handleLine, Config.get("service-socket.socket"));
}
116 changes: 116 additions & 0 deletions servcmd.sh.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env node
/*
** CyTube Service Socket Commandline
*/

const readline = require('readline');
const spawn = require('child_process').spawn;
const util = require('util');
const net = require('net');
const fs = require('fs');

const COMPLETIONS = [
"/delete_old_tables",
"/gc",
"/globalban",
"/reload",
"/reload-partitions",
"/switch",
"/unglobalban",
"/unloadchan"
];

var Config = require("./lib/config");
Config.load("config.yaml");

if(!Config.get("service-socket.enabled")){
console.error('The Service Socket is not enabled.');
process.exit(1);
}

const SOCKETFILE = Config.get("service-socket.socket");

// Wipe the TTY
process.stdout.write('\x1Bc');

var commandline, eventlog, syslog;
var client = net.createConnection(SOCKETFILE).on('connect', () => {
commandline = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: tabcomplete
});
commandline.setPrompt("> ", 2);
commandline.on("line", function(line) {
if(line === 'exit'){ return cleanup(); }
if(line === 'quit'){ return cleanup(); }
if(line.match(/^\/globalban/) && line.split(/\s+/).length === 2){
console.log('You must provide a reason')
return commandline.prompt();
}
client.write(line);
commandline.prompt();
});
commandline.on('close', function() {
return cleanup();
});
commandline.on("SIGINT", function() {
commandline.clearLine();
commandline.question("Terminate connection? ", function(answer) {
return answer.match(/^y(es)?$/i) ? cleanup() : commandline.output.write("> ");
});
});
commandline.prompt();

console.log = function() { cmdouthndlr("log", arguments); }
console.warn = function() { cmdouthndlr("warn", arguments); }
console.error = function() { cmdouthndlr("error", arguments); }
// console.info is reserved in this script for the exit message
// this prevents an extraneous final prompt from readline on terminate

eventlog = spawn('tail', ['-f', 'events.log']);
eventlog.stdout.on('data', function (data) {
console.log(data.toString().replace(/^(.+)$/mg, 'events: $1'));
});

syslog = spawn('tail', ['-f', 'sys.log']);
syslog.stdout.on('data', function (data) {
console.log(data.toString().replace(/^(.+)$/mg, 'sys: $1'));
});

}).on('data', (msg) => {
msg = msg.toString();

if(msg === '__disconnect'){
console.log('Server shutting down.');
return cleanup();
}

// Generic message handler
console.log('server: ', data)

}).on('error', (data) => {
console.error('Unable to connect to Service Socket.', data);
process.exit(1);
});

function cmdouthndlr(type, args) {
var t = Math.ceil((commandline.line.length + 3) / process.stdout.columns);
var text = util.format.apply(console, args);
commandline.output.write("\n\x1B[" + t + "A\x1B[0J");
commandline.output.write(text + "\n");
commandline.output.write(Array(t).join("\n\x1B[E"));
commandline._refreshLine();
}

function cleanup(){
console.info('\n',"Terminating.",'\n');
eventlog.kill('SIGTERM');
syslog.kill('SIGTERM');
client.end();
process.exit(0);
}

function tabcomplete(line) {
return [COMPLETIONS.filter((cv)=>{ return cv.indexOf(line) == 0; }), line];
}
4 changes: 4 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ var defaults = {
},
"channel-storage": {
type: "file"
},
"service-socket": {
enabled: false,
socket: "service.sock"
}
};

Expand Down
53 changes: 53 additions & 0 deletions src/servsock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
var fs = require('fs');
var net = require('net');

export default class ServiceSocket {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this class need error event handlers? In case the client connection breaks or something.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have thus far been unable to break anything. Sending SIGKILL to the client leaves orphaned children (the tail on the log files) but doesn't affect the service at all. Sending SIGTERM to the service after SIGKILL'ing a client shuts down the service with no errors.


constructor() {
this.connections = {};
}

init(handler, socket){
this.handler = handler;
this.socket = socket;

fs.stat(this.socket, (err, stats) => {
if (err) {
return this.openServiceSocket();
}
fs.unlink(this.socket, (err) => {
if(err){
console.error(err); process.exit(0);
}
return this.openServiceSocket();
});
});
}

openServiceSocket(){
this.server = net.createServer((stream) => {
let id = Date.now();
this.connections[id] = stream;
stream.on('end', () => {
delete this.connections[id];
});
stream.on('data', (msg) => {
this.handler(msg.toString());
});
}).listen(this.socket);
process.on('exit', this.closeServiceSocket.bind(this));
}

closeServiceSocket() {
if(Object.keys(this.connections).length){
let clients = Object.keys(this.connections);
while(clients.length){
let client = clients.pop();
this.connections[client].write('__disconnect');
this.connections[client].end();
}
}
this.server.close();
}

}