Skip to content

Commit

Permalink
Sockets: Go rewrite
Browse files Browse the repository at this point in the history
Work in progress. I haven't written documentation for the Go code or
confirmed whether this works as intended on Windows yet.

This turned into a pretty substantial refactor of the Node version of
sockets-related code to not only to make using Go child processes
possible, but to optimize it and make unit testing of it and any code
dependent on it possible to write entirely synchronously. sockets.js
and sockets-workers.js are now written in Typescript, though they won't
be able to be transpiled until after Config, Users, Dnsbl, and Monitor
work with it as well.

Fixes smogon#2943
  • Loading branch information
Morfent committed Sep 17, 2017
1 parent b365d3f commit 9445c21
Show file tree
Hide file tree
Showing 21 changed files with 2,725 additions and 690 deletions.
20 changes: 19 additions & 1 deletion config/config-example.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,32 @@
// The server port - the port to run Pokemon Showdown under
exports.port = 8000;

// The server bind address - the address to run Pokemon Showdown under
// This should be left set to 0.0.0.0 unless you know what you are doing.
exports.bindaddress = '0.0.0.0';

// workers - the number of sockets workers to spawn
// This should not be set any higher than the number of cores available on
// the server's CPU(s). This can be checked from a REPL using
// require('os').cpus().length if you are unsure.
exports.workers = 1;

// golang - toggle using Go instead of Node for sockets workers
// Node workers are more unstable at handling connections because of bugs in
// sockjs-node, but sending/receiving messages over connections on Go workers
// is slightly slower due to the extra work involved in performing IPC with
// them safely. This should be left set to false unless you know what you are
// doing.
exports.golang = false;

// proxyip - proxy IPs with trusted X-Forwarded-For headers
// This can be either false (meaning not to trust any proxies) or an array
// of strings. Each string should be either an IP address or a subnet given
// in CIDR notation. You should usually leave this as `false` unless you
// know what you are doing.
exports.proxyip = false;

// ofe - write heapdumps if sockets.js workers run out of memory.
// ofe - write heapdumps if Node sockets workers run out of memory
// If you wish to enable this, you will need to install ofe, as it is not a
// installed by default:
// $ npm install --no-save ofe
Expand Down
35 changes: 35 additions & 0 deletions dev-tools/sockets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const {Session, SockJSConnection} = require('sockjs/lib/transport');

const chars = 'abcdefghijklmnopqrstuvwxyz1234567890-';
let sessionidCount = 0;

/**
* @return string
*/
function generateSessionid() {
let ret = '';
let idx = sessionidCount;
for (let i = 0; i < 8; i++) {
ret = chars[idx % chars.length] + ret;
idx = idx / chars.length | 0;
}
sessionidCount++;
return ret;
}

/**
* @param {string} sessionid
* @param {{options: {{}}} config
* @return SockJSConnection
*/
exports.createSocket = function (sessionid = generateSessionid(), config = {options: {}}) {
let session = new Session(sessionid, config);
let socket = new SockJSConnection(session);
socket.remoteAddress = '127.0.0.1';
socket.protocol = 'websocket';
return socket;
};

// TODO: move worker mocks here, use require('../sockets-workers').Multiplexer to stub IPC
55 changes: 54 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
"private": true,
"license": "MIT",
"devDependencies": {
"@types/cloud-env": "^0.2.0",
"@types/node": "^8.0.1",
"@types/node-static": "^0.7.0",
"@types/nodemailer": "^1.3.33",
"@types/ofe": "^0.5.0",
"@types/sockjs": "^0.3.31",
"eslint": "^4.0.0",
"mocha": "^3.0.0",
"@types/node": "^8.0.1",
Expand Down
128 changes: 110 additions & 18 deletions pokemon-showdown
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,119 @@ try {
);
}

if (!process.argv[2] || /^[0-9]+$/.test(process.argv[2])) {
// Start the server. We manually load app.js so it can be configured to run as
// the main module, rather than this file being considered the main module.
// This ensures any dependencies that were just installed can be found when
// running on Windows and avoids any other potential side effects of the main
// module not being app.js like it is assumed to be.
//
// The port the server should host on can be passed using the second argument
// when launching with this file the same way app.js normally allows, e.g. to
// host on port 9000:
// $ ./pokemon-showdown 9000

require('module')._load('./app', module, true);
} else switch (process.argv[2]) {
// ALlow arguments passed to the launch script to be evaluated as commands.
let [, , arg2, arg3, arg4] = process.argv;
if (arg2 && /^[0-9]$/.test(arg2)) {
switch (arg2) {
case 'generate-team':
const Dex = require('./sim/dex');
global.toId = Dex.getId;
const seed = process.argv[4] ? process.argv[4].split(',').map(Number) : undefined;
console.log(Dex.packTeam(Dex.generateTeam(process.argv[3], seed)));
break;
const seed = arg4 ? arg4.split(',').map(Number) : undefined;
console.log(Dex.packTeam(Dex.generateTeam(arg3, seed)));
process.exit(0);
default:
console.error('Unrecognized command: ' + process.argv[2]);
console.error(`Unrecognized command: ${arg2}`);
process.exit(1);
}
}

// If evaluating commands wasn't the point of running this script, let's launch
// the server.

// Check if the server is configured to use Go, and ensure the required
// environment variables and dependencies are available if that is the case

let config;
try {
config = require('./config/config');
} catch (e) {}

if (config && config.golang) {
// GOPATH and GOROOT are optional to a degree, but we need them in order
// to be able to handle Go dependencies. Since Go only cares about the
// first path in the list, so will we.
const GOPATH = child_process.execSync('go env GOPATH', {stdio: null, encoding: 'utf8'})
.trim()
.split(path.delimiter)[0]
.replace(/^"(.*)"$/, '$1');
if (!GOPATH) {
// Should never happen, but it does on Bash on Ubuntu on Windows.
console.error('There is no $GOPATH environment variable set. Run:');
console.error('$ go help GOPATH');
console.error('For more information on how to configure it.');
process.exit(0);
}

const dependencies = ['github.com/gorilla/mux', 'github.com/igm/sockjs-go/sockjs'];
let packages = child_process.execSync('go list all', {stdio: null, encoding: 'utf8'});
for (let dep of dependencies) {
if (!packages.includes(dep)) {
console.log(`Dependency ${dep} is not installed. Fetching...`);
child_process.execSync(`go get ${dep}`, {stdio: 'inherit'});
}
}

let stat;
let needsSrcDir = false;
try {
stat = fs.lstatSync(path.resolve(GOPATH, 'src/github.com/Zarel'));
} catch (e) {
needsSrcDir = true;
} finally {
if (stat && !stat.isDirectory()) {
needsSrcDir = true;
}
}

let srcPath = path.resolve(process.cwd(), 'sockets');
let tarPath = path.resolve(GOPATH, 'src/github.com/Zarel/Pokemon-Showdown/sockets');
if (needsSrcDir) {
try {
fs.mkdirSync(path.resolve(GOPATH, 'src/github.com/Zarel'));
fs.mkdirSync(path.resolve(GOPATH, 'src/github.com/Zarel/Pokemon-Showdown'));
} catch (e) {
console.error(e);
console.error(`Cannot make go source directory for the sockets library files! Symlink them manually from ${srcPath} to ${tarPath}`);
process.exit(0);
}
}

try {
stat = fs.lstatSync(path.resolve(GOPATH, 'src/github.com/Zarel/Pokemon-Showdown/sockets'));
} catch (e) {}

if (!stat || !stat.isSymbolicLink()) {
// Windows requires administrator privileges to make symlinks, so we
// make junctions instead. For our purposes they're compatible enough
// with symlinks on UNIX-like OSes.
let symlinkType = (process.platform === 'win32') ? 'junction' : 'dir';
try {
fs.symlinkSync(srcPath, tarPath, symlinkType);
} catch (e) {
console.error(`Cannot make go source directory for the sockets library files! Symlink them manually from ${srcPath} to ${tarPath}`);
process.exit(0);
}
}

console.log('Building Go source libs...');
try {
child_process.execSync('go install github.com/Zarel/Pokemon-Showdown/sockets', {stdio: 'inherit'});
} catch (e) {
// Go will show the errors that caused compiling Go's files to fail, so
// there's no reason to bother logging anything of our own.
process.exit(0);
}
}

// Start the server. We manually load app.js so it can be configured to run as
// the main module, rather than this file being considered the main module.
// This ensures any dependencies that were just installed can be found when
// running on Windows and avoids any other potential side effects of the main
// module not being app.js like it is assumed to be.
//
// The port the server should host on can be passed using the second argument
// when launching with this file the same way app.js normally allows, e.g. to
// host on port 9000:
// $ ./pokemon-showdown 9000

require('module')._load('./app', module, true);
Loading

0 comments on commit 9445c21

Please sign in to comment.