Permalink
Browse files

Smaller API for running/monitoring workers

  • Loading branch information...
1 parent 5b2e369 commit e1317ced5204d1292381dc45dc08c377bfe391c2 @jcheng5 jcheng5 committed Dec 1, 2012
Showing with 246 additions and 1 deletion.
  1. +1 −0 .gitignore
  2. +8 −0 binding.gyp
  3. +8 −0 lib/worker/app-spec.js
  4. +115 −0 lib/worker/app-worker.js
  5. +37 −0 lib/worker/run-as.js
  6. +4 −1 package.json
  7. +56 −0 src/posix.cc
  8. +17 −0 test/test-app-worker.js
View
@@ -1,2 +1,3 @@
+build/
node_modules/
npm-debug.log
View
@@ -0,0 +1,8 @@
+{
+ "targets": [
+ {
+ "target_name": "posix",
+ "sources": [ "src/posix.cc" ]
+ }
+ ]
+}
@@ -0,0 +1,8 @@
+var AppSpec = function(appDir, runAs, logDir, settings) {
+ this.appDir = appDir;
+ this.runAs = runAs;
+ this.logDir = logDir;
+ this.settings = settings;
+}
+
+module.exports = AppSpec;
@@ -0,0 +1,115 @@
+/**
+ * An AppWorker is responsible for:
+ *
+ * - Launching a Shiny application with the proper user/group permissions
+ * - Ensuring that stderr is written to the specified path
+ * - Returning a promise that resolves when the worker process exits
+ */
+
+var child_process = require('child_process');
+var fs = require('fs');
+var util = require('util');
+var Q = require('q');
+var _ = require('underscore');
+
+var rprog = process.env.R || 'R';
+var scriptPath = __dirname + '/../../R/SockJSAdapter.R';
+
+/**
+ * Begins launching the worker; returns a promise that resolves when
+ * the worker is constructed (doesn't necessarily mean the process has
+ * actually begun running though).
+ *
+ * @param {AppSpec} appSpec - Contains the basic details about the app to
+ * launch
+ * @param {Number} listenPort - The port number that the Shiny app should use.
+ * @param {String} logFilePath - The file path to write stderr to.
+ */
+var launchWorker_p = exports.launchWorker_p = function(appSpec, listenPort, logFilePath) {
+
+ // Open the log file asynchronously, then create the worker
+
+ return Q.nfcall(fs.open, logFilePath, 'a', 0666).then(function(logStream) {
+
+ // Create the worker; when it exits (or fails to start), close
+ // the logStream.
+ var worker = new AppWorker(appSpec, listenPort, logStream);
+ worker.getExit_p().fin(function() {
+ logStream.end();
+ });
+
+ return worker;
+ });
+};
+
+/**
+ * Like launchWorker_p, but the promise it returns doesn't resolve until
+ * the worker process exits.
+ */
+exports.runWorker_p = function(appSpec, listenPort, logFilePath) {
+ return launchWorker_p(appSpec, listenPort, logFilePath).then(function(worker) {
+ return worker.getExit_p();
+ });
+};
+
+/**
+ * An AppWorker models a single R process that is running a Shiny app.
+ *
+ * @constructor
+ * @param {AppSpec} appSpec - Contains the basic details about the app to
+ * launch
+ * @param {Number} listenPort - The port number that the Shiny app should use.
+ * @param {Stream} logStream - The stream to dump stderr to.
+ */
+var AppWorker = function(appSpec, listenPort, logStream) {
+ this.$dfEnded = Q.defer();
+ var self = this;
+
+ try {
+ // Run R
+ if (!appSpec.runAs)
+ throw new Exception("No user specified");
+
+ if (!appSpec.appDir)
+ throw new Exception("No app directory specified");
+
+ var args = [
+ "--no-save",
+ "-f",
+ scriptPath
+ ];
+
+ var env = _.clone(process.env);
+ env.SHINY_PORT = '' + listenPort;
+ env.SHINY_APP = '.';
+ if (appSpec.settings.gaTrackingId)
+ env.SHINY_GAID = appSpec.settings.gaTrackingId;
+
+ this.$proc = child_process.spawn(rprog, args, {
+ env: env,
+ cwd: appSpec.appDir,
+ stdio: ['ignore', 'ignore', logStream]
+ });
+ this.$proc.on('exit', function(code, signal) {
+ self.$dfEnded.resolve({code: code, signal: signal});
+ });
+ }
+ catch (e) {
+ this.$dfEnded.reject(e);
+ }
+};
+
+(function() {
+
+ /**
+ * Returns a promise that is resolved when the process exits.
+ * If the process terminated normally, code is the final exit
+ * code of the process, otherwise null. If the process
+ * terminated due to receipt of a signal, signal is the string
+ * name of the signal, otherwise null.
+ */
+ this.getExit_p = function() {
+ return this.$dfEnded.promise;
+ };
+
+}).call(AppWorker.prototype);
View
@@ -0,0 +1,37 @@
+// Intended to be launched via child_process.exec. It merely drops
+// permissions and runs the given command/args. stdio is passed
+// through, and the exit code of the subcommand will also be the
+// exit code of this command.
+//
+// Usage: run-as.js <user> <command> <args>
+
+var unixgroups = require('unixgroups');
+var child_process = require('child_process');
+var _ = require('underscore');
+var posix = require('../../build/Release/posix');
+
+
+// Drop permissions first
+var user = process.argv[2];
+unixgroups.initgroups(user, true); // also calls setgid
+process.setuid(user);
+
+
+var cmd = process.argv[3];
+var args = _.rest(process.argv, 4);
+
+// Change a few env variables to match user's identity
+var env = _.clone(process.env);
+var pwd = posix.getpwnam(user);
+env.USER = user;
+env.HOME = pwd.home;
+env.SHELL = pwd.shell;
+
+var proc = child_process.spawn(cmd, args, {
+ stdio: 'inherit',
+ env: env
+});
+
+proc.on('exit', function(code) {
+ process.exit(code);
+});
View
@@ -14,7 +14,10 @@
"sockjs" : "0.3.1",
"cjson" : "0.2.1",
"handlebars" : "1.0.7",
- "underscore" : "1.4.2"
+ "underscore" : "1.4.2",
+ "q" : "0.8.x",
+ "bash" : "0.0.1",
+ "unixgroups" : "https://github.com/rstudio/node-unixgroups/tarball/5a348df193c"
},
"license": "AGPL-3",
"engines": {
View
@@ -0,0 +1,56 @@
+#include <node.h>
+#include <v8.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <pwd.h>
+
+using namespace node;
+using namespace v8;
+
+Handle<Value> GetPwNam(const Arguments& args) {
+ HandleScope scope;
+
+ if (args.Length() < 1) {
+ return ThrowException(Exception::Error(
+ String::New("getpwnam requires 1 argument")));
+ }
+
+ String::Utf8Value pwnam(args[0]);
+
+ int err = 0;
+ struct passwd pwd;
+ struct passwd *pwdp = NULL;
+
+ int bufsize = sysconf(_SC_GETPW_R_SIZE_MAX);
+ if (bufsize == -1) // value was indeterminant
+ bufsize = 16384;
+ char buf[bufsize];
+
+ errno = 0;
+ if ((err = getpwnam_r(*pwnam, &pwd, buf, bufsize, &pwdp)) || pwdp == NULL) {
+ if (errno == 0)
+ return ThrowException(Exception::Error(
+ String::New("getpwnam user does not exist")));
+ else
+ return ThrowException(ErrnoException(errno, "getpwnam_r"));
+ }
+
+ Local<Object> userInfo = Object::New();
+ userInfo->Set(String::NewSymbol("name"), String::New(pwd.pw_name));
+ userInfo->Set(String::NewSymbol("passwd"), String::New(pwd.pw_passwd));
+ userInfo->Set(String::NewSymbol("uid"), Number::New(pwd.pw_uid));
+ userInfo->Set(String::NewSymbol("gid"), Number::New(pwd.pw_gid));
+ userInfo->Set(String::NewSymbol("gecos"), String::New(pwd.pw_gecos));
+ userInfo->Set(String::NewSymbol("home"), String::New(pwd.pw_dir));
+ userInfo->Set(String::NewSymbol("shell"), String::New(pwd.pw_shell));
+
+ return scope.Close(userInfo);
+}
+
+void Initialize(Handle<Object> target) {
+ target->Set(String::NewSymbol("getpwnam"),
+ FunctionTemplate::New(GetPwNam)->GetFunction());
+}
+NODE_MODULE(posix, Initialize)
@@ -0,0 +1,17 @@
+var util = require('util');
+var AppSpec = require('../lib/worker/app-spec');
+var app_worker = require('../lib/worker/app-worker');
+
+var rw_p = app_worker.runWorker_p(new AppSpec(
+ '/Users/jcheng/ShinyApps/diamonds', 'jcheng', null, {}),
+ 8103, './testlog.log');
+
+rw_p
+.then(
+ function(status) {
+ console.log('exit with status: ' + util.inspect(status));
+ },
+ function(err) {
+ console.log('err: ' + err);
+ }
+);

0 comments on commit e1317ce

Please sign in to comment.