Skip to content

Commit

Permalink
Merge pull request meteor#8702 from vlasky/devel
Browse files Browse the repository at this point in the history
Support for UNIX sockets (meteor#7392)
  • Loading branch information
benjamn authored Sep 6, 2017
2 parents 5f13b20 + bcc7835 commit 0d7ce02
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 43 deletions.
11 changes: 11 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
## v.NEXT

* The `webapp` package has been updated to support UNIX domain sockets. If a
`UNIX_SOCKET_PATH` environment variable is set with a valid
UNIX socket file path (e.g. `UNIX_SOCKET_PATH=/tmp/socktest.sock`), Meteor's
HTTP server will use that socket file for inter-process communication,
instead of TCP. This can be useful in cases like using Nginx to proxy
requests back to an internal Meteor application. Leveraging UNIX domain
sockets for inter-process communication reduces the sometimes unnecessary
overhead required by TCP based communication.
[Issue #7392](https://github.com/meteor/meteor/issues/7392)
[PR #8702](https://github.com/meteor/meteor/pull/8702)

* The `fastclick` package (previously included by default in Cordova
applications through the `mobile-experience` package) has been deprecated.
This package is no longer maintained and has years of outstanding
Expand Down
1 change: 1 addition & 0 deletions packages/webapp/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ Package.onTest(function (api) {
api.use(['tinytest', 'ecmascript', 'webapp', 'http', 'underscore']);
api.addFiles('webapp_tests.js', 'server');
api.addFiles('webapp_client_tests.js', 'client');
api.addFiles('socket_file_tests.js', 'server');
});
61 changes: 61 additions & 0 deletions packages/webapp/socket_file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { statSync, unlinkSync, existsSync } from 'fs';

// Since a new socket file will be created when the HTTP server
// starts up, if found remove the existing file.
//
// WARNING:
// This will remove the configured socket file without warning. If
// the configured socket file is already in use by another application,
// it will still be removed. Node does not provide a reliable way to
// differentiate between a socket file that is already in use by
// another application or a stale socket file that has been
// left over after a SIGKILL. Since we have no reliable way to
// differentiate between these two scenarios, the best course of
// action during startup is to remove any existing socket file. This
// is not the safest course of action as removing the existing socket
// file could impact an application using it, but this approach helps
// ensure the HTTP server can startup without manual
// intervention (e.g. asking for the verification and cleanup of socket
// files before allowing the HTTP server to be started).
//
// The above being said, as long as the socket file path is
// configured carefully when the application is deployed (and extra
// care is taken to make sure the configured path is unique and doesn't
// conflict with another socket file path), then there should not be
// any issues with this approach.
export const removeExistingSocketFile = (socketPath) => {
try {
if (statSync(socketPath).isSocket()) {
// Since a new socket file will be created, remove the existing
// file.
unlinkSync(socketPath);
} else {
throw new Error(
`An existing file was found at "${socketPath}" and it is not ` +
'a socket file. Please confirm PORT is pointing to valid and ' +
'un-used socket file path.'
);
}
} catch (error) {
// If there is no existing socket file to cleanup, great, we'll
// continue normally. If the caught exception represents any other
// issue, re-throw.
if (error.code !== 'ENOENT') {
throw error;
}
}
};

// Remove the socket file when done to avoid leaving behind a stale one.
// Note - a stale socket file is still left behind if the running node
// process is killed via signal 9 - SIGKILL.
export const registerSocketFileCleanup =
(socketPath, eventEmitter = process) => {
['exit', 'SIGINT', 'SIGHUP', 'SIGTERM'].forEach(signal => {
eventEmitter.on(signal, Meteor.bindEnvironment(() => {
if (existsSync(socketPath)) {
unlinkSync(socketPath);
}
}));
});
};
69 changes: 69 additions & 0 deletions packages/webapp/socket_file_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { writeFileSync, unlinkSync, statSync } from 'fs';
import { createServer } from 'net';
import {
removeExistingSocketFile,
registerSocketFileCleanup,
} from './socket_file.js';
import { EventEmitter } from 'events';
import { tmpdir } from 'os';

const testSocketFile = `${tmpdir()}/socket_file_tests`;

const removeTestSocketFile = () => {
try {
unlinkSync(testSocketFile);
} catch (error) {
// Do nothing
}
}

Tinytest.add("socket file - don't remove a non-socket file", test => {
writeFileSync(testSocketFile);
test.throws(
() => { removeExistingSocketFile(testSocketFile); },
/An existing file was found/
);
removeTestSocketFile()
});

Tinytest.addAsync(
'socket file - remove a previously existing socket file',
(test, done) => {
removeTestSocketFile();
const server = createServer();
server.listen(testSocketFile);

server.on('listening', Meteor.bindEnvironment(() => {
test.isNotUndefined(statSync(testSocketFile));
removeExistingSocketFile(testSocketFile);
test.throws(
() => { statSync(testSocketFile); },
/ENOENT/
);
server.close();
done();
}));
}
);

Tinytest.add(
'socket file - no existing socket file, nothing to remove',
test => {
removeTestSocketFile();
removeExistingSocketFile(testSocketFile);
}
);

Tinytest.add('socket file - remove socket file on exit', test => {
const testEventEmitter = new EventEmitter();
registerSocketFileCleanup(testSocketFile, testEventEmitter);
['exit', 'SIGINT', 'SIGHUP', 'SIGTERM'].forEach(signal => {
writeFileSync(testSocketFile);
test.isNotUndefined(statSync(testSocketFile));
testEventEmitter.emit(signal);
test.throws(
() => { statSync(testSocketFile); },
/ENOENT/
);
});
});
78 changes: 53 additions & 25 deletions packages/webapp/webapp_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import connect from "connect";
import parseRequest from "parseurl";
import { lookup as lookupUserAgent } from "useragent";
import send from "send";
import {
removeExistingSocketFile,
registerSocketFileCleanup,
} from './socket_file.js';

var SHORT_SOCKET_TIMEOUT = 5*1000;
var LONG_SOCKET_TIMEOUT = 120*1000;
Expand Down Expand Up @@ -266,7 +270,7 @@ WebAppInternals.registerBoilerplateDataCallback = function (key, callback) {
//
// If a previous connect middleware has rendered content for the head or body,
// returns the boilerplate with that content patched in otherwise
// memoizes on HTML attributes (used by, eg, appcache) and whether inline
// memoizes on HTML attributes (used by, eg, appcache) and whether inline
// scripts are currently allowed.
// XXX so far this function is always called with arch === 'web.browser'
var memoizedBoilerplate = {};
Expand Down Expand Up @@ -486,14 +490,19 @@ var getUrlPrefixForArch = function (arch) {
'' : '/' + '__' + arch.replace(/^web\./, '');
};

// parse port to see if its a Windows Server style named pipe. If so, return as-is (String), otherwise return as Int
WebAppInternals.parsePort = function (port) {
if( /\\\\?.+\\pipe\\?.+/.test(port) ) {
return port;
// Parse the passed in port value. Return the port as-is if it's a String
// (e.g. a Windows Server style named pipe), otherwise return the port as an
// integer.
//
// DEPRECATED: Direct use of this function is not recommended; it is no
// longer used internally, and will be removed in a future release.
WebAppInternals.parsePort = port => {
let parsedPort = parseInt(port);
if (Number.isNaN(parsedPort)) {
parsedPort = port;
}

return parseInt(port);
};
return parsedPort;
}

function runWebAppServer() {
var shuttingDown = false;
Expand Down Expand Up @@ -856,28 +865,47 @@ function runWebAppServer() {
// Let the rest of the packages (and Meteor.startup hooks) insert connect
// middlewares and update __meteor_runtime_config__, then keep going to set up
// actually serving HTML.
exports.main = function (argv) {
exports.main = argv => {
WebAppInternals.generateBoilerplate();

// only start listening after all the startup code has run.
var localPort = WebAppInternals.parsePort(process.env.PORT) || 0;
var host = process.env.BIND_IP;
var localIp = host || '0.0.0.0';
httpServer.listen(localPort, localIp, Meteor.bindEnvironment(function() {
if (process.env.METEOR_PRINT_ON_LISTEN) {
console.log("LISTENING"); // must match run-app.js
}
const startHttpServer = listenOptions => {
httpServer.listen(listenOptions, Meteor.bindEnvironment(() => {
if (process.env.METEOR_PRINT_ON_LISTEN) {
console.log("LISTENING");
}
const callbacks = onListeningCallbacks;
onListeningCallbacks = null;
callbacks.forEach(callback => { callback(); });
}, e => {
console.error("Error listening:", e);
console.error(e && e.stack);
}));
};

var callbacks = onListeningCallbacks;
onListeningCallbacks = null;
_.each(callbacks, function (x) { x(); });
let localPort = process.env.PORT || 0;
const host = process.env.BIND_IP;
const localIp = host || "0.0.0.0";
const unixSocketPath = process.env.UNIX_SOCKET_PATH;

}, function (e) {
console.error("Error listening:", e);
console.error(e && e.stack);
}));
if (unixSocketPath) {
// Start the HTTP server using a socket file.
removeExistingSocketFile(unixSocketPath);
startHttpServer({ path: unixSocketPath });
registerSocketFileCleanup(unixSocketPath);
} else {
localPort = isNaN(Number(localPort)) ? localPort : Number(localPort);
if (/\\\\?.+\\pipe\\?.+/.test(localPort)) {
// Start the HTTP server using Windows Server style named pipe.
startHttpServer({ path: localPort });
} else if (typeof localPort === "number") {
// Start the HTTP server using TCP.
startHttpServer({ port: localPort, host: host });
} else {
throw new Error("Invalid PORT specified");
}
}

return 'DAEMON';
return "DAEMON";
};
}

Expand Down
51 changes: 34 additions & 17 deletions packages/webapp/webapp_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,23 +194,40 @@ Tinytest.add("webapp - WebAppInternals.registerBoilerplateDataCallback", functio
);
});

// Support 'named pipes' (strings) as ports for support of Windows Server / Azure deployments
Tinytest.add("webapp - port should be parsed as int unless it is a named pipe", function(test){
// Named pipes on Windows Server follow the format: \\.\pipe\{randomstring} or \\{servername}\pipe\{randomstring}
var namedPipe = "\\\\.\\pipe\\b27429e9-61e3-4c12-8bfe-950fa3295f74";
var namedPipeServer = "\\\\SERVERNAME-1234\\pipe\\6e157e98-faef-49e4-a0cf-241037223308";

test.equal(WebAppInternals.parsePort(namedPipe),
"\\\\.\\pipe\\b27429e9-61e3-4c12-8bfe-950fa3295f74");
test.equal(WebAppInternals.parsePort(namedPipeServer),
"\\\\SERVERNAME-1234\\pipe\\6e157e98-faef-49e4-a0cf-241037223308");
test.equal(WebAppInternals.parsePort(8080),
8080);
test.equal(WebAppInternals.parsePort("8080"),
8080);
test.equal(WebAppInternals.parsePort("8080abc"), // ensure strangely formatted ports still work for backwards compatibility
8080);
});
// Support 'named pipes' (strings) as ports for support of Windows Server /
// Azure deployments
Tinytest.add(
"webapp - port should be parsed as int unless it is a named pipe",
function (test) {
// Named pipes on Windows Server follow the format:
// \\.\pipe\{randomstring} or \\{servername}\pipe\{randomstring}
var namedPipe = "\\\\.\\pipe\\b27429e9-61e3-4c12-8bfe-950fa3295f74";
var namedPipeServer =
"\\\\SERVERNAME-1234\\pipe\\6e157e98-faef-49e4-a0cf-241037223308";

test.equal(
WebAppInternals.parsePort(namedPipe),
"\\\\.\\pipe\\b27429e9-61e3-4c12-8bfe-950fa3295f74"
);
test.equal(
WebAppInternals.parsePort(namedPipeServer),
"\\\\SERVERNAME-1234\\pipe\\6e157e98-faef-49e4-a0cf-241037223308"
);
test.equal(
WebAppInternals.parsePort(8080),
8080
);
test.equal(
WebAppInternals.parsePort("8080"),
8080
);
// Ensure strangely formatted ports still work for backwards compatibility
test.equal(
WebAppInternals.parsePort("8080abc"),
8080
);
}
);

__meteor_runtime_config__.WEBAPP_TEST_A = '<p>foo</p>';
__meteor_runtime_config__.WEBAPP_TEST_B = '</script>';
Expand Down
9 changes: 8 additions & 1 deletion tools/runners/run-all.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,14 @@ class Runner {

if (! self.stopped && ! self.quiet) {
runLog.log("");
runLog.log("App running at: " + self.rootUrl, { arrow: true });
if (process.env.UNIX_SOCKET_PATH) {
runLog.log(
`App running; UNIX domain socket: ${process.env.UNIX_SOCKET_PATH}`,
{ arrow: true }
);
} else {
runLog.log("App running at: " + self.rootUrl, { arrow: true });
}

if (process.platform === "win32") {
runLog.log(" Type Control-C twice to stop.");
Expand Down

0 comments on commit 0d7ce02

Please sign in to comment.