c9-core/plugins/c9.ide.run/run.js

1165 wiersze
48 KiB
JavaScript
Czysty Wina Historia

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

define(function(require, module, exports) {
main.consumes = [
"Plugin", "proc", "settings", "fs", "c9", "util", "http", "info"
];
main.provides = ["run"];
return main;
function main(options, imports, register) {
var Plugin = imports.Plugin;
var settings = imports.settings;
var proc = imports.proc;
var util = imports.util;
var fs = imports.fs;
var c9 = imports.c9;
var info = imports.info;
var basename = require("path").basename;
var dirname = require("path").dirname;
var join = require("path").join;
/***** Initialization *****/
var handle = new Plugin("Ajax.org", main.consumes);
var handleEmit = handle.getEmitter();
var CLEANING = -2;
var STOPPING = -1;
var STOPPED = 0;
var STARTING = 1;
var STARTED = 2;
var STATIC = options.staticPrefix;
var installPath = options.installPath || "~/.c9";
var TMUX = options.tmux || installPath + "/bin/tmux";
var BASH = "bash";
var runners = util.cloneObject(options.runners);
var testing = options.testing;
var runnerPath = options.runnerPath || "/.c9/runners";
var base = (options.base || "").replace(/\/?$/, "/");
var workspace = info.getWorkspace();
var processes = [];
var loaded = false;
function load() {
if (loaded) return false;
loaded = true;
// Settings
settings.on("read", function(e) {
// Defaults
settings.setDefaults("project/run", [
["path", runnerPath]
]);
}, handle);
// @todo Could consider adding a watcher to ~/.c9/runners
}
/***** Methods *****/
function addRunner(name, runner, plugin) {
runners[name] = runner;
plugin.addOther(function() { delete runners[name]; });
}
function listRunners(callback) {
var _runners = Object.keys(runners || {});
fs.exists(settings.get("project/run/@path"), function(exists) {
if (!exists)
return callback(null, _runners);
fs.readdir(settings.get("project/run/@path"), function(err, files) {
// if (err && err.code == "ENOENT")
// return callback(err);
if (files) {
files.forEach(function(file) {
var name = file.name.match(/(.*)\.run$/);
if (!name) {
if (file.name.match(/\.\w+$/))
return console.warn("Runner ignored, doesn't have .run extension: " + file.name);
name = [0, file.name];
}
if (_runners.indexOf(name[1]) < 0)
_runners.push(name[1]);
});
}
callback(null, _runners);
});
});
}
function detectRunner(options, callback) {
listRunners(function(err, names) {
if (err) return callback(err);
var count = 0;
names.forEach(function(name) {
if (!runners[name]) {
count++;
getRunner(name, false, function() {
if (--count === 0)
done();
});
}
});
if (count === 0) done();
});
function done() {
for (var name in runners) {
var runner = runners[name];
if (runner.python_version
&& runner.python_version !== settings.get("project/python/@version"))
continue;
if (matchSelector(runner.selector, options.path))
return callback(null, runner);
}
var err = new Error("Could not find Runner");
err.code = "ERUNNERNOTFOUND";
callback(err);
}
}
function matchSelector(selector, path) {
if (typeof selector == "string") {
if (selector.indexOf("source.") === 0)
return selector == "source." + fs.getExtension(path);
else {
var re = new RegExp(selector);
var file = basename(path);
return re.test(file);
}
}
else if (Array.isArray(selector)) {
return selector.some(function(n) {
return matchSelector(n, path);
});
}
else {
return false;
}
}
function getRunner(name, refresh, callback) {
if (typeof refresh == "function") {
callback = refresh;
refresh = false;
}
// Fix legacy runner names
if (name === "Python 2.7")
name = "Python 2";
if (name === "Python 3.4")
name = "Python 3";
// When runner is loaded and we don't require a refresh
if (runners[name] && refresh === false) {
return done(runners[name]);
}
// Search for <name>.run or <name> and load
var path = settings.get("project/run/@path") + "/" + name + ".run";
fs.exists(path, function test(exists) {
if (!exists) {
if (/\.run$/.test(path)) {
path = settings.get("project/run/@path") + "/" + name;
return fs.exists(path, test);
}
callback("Runner does not exist");
} else {
fs.readFile(path, "utf8", function(err, data) {
if (err) return callback(err);
var runner = util.safeParseJson(data, callback);
if (!runner) return;
runner.caption = runner.caption || name.replace(/\.run$/, "");
runners[runner.caption] = runner;
done(runner);
});
}
});
function done(runner) {
callback(null, runner);
}
}
function restoreProcess(state) {
var process = new Process(state);
handleEmit("create", { process: process });
return process;
}
function makeAbsolutePath(path) {
if (!path) return path;
if (path.charAt(0) === "~")
return join(c9.home, path.substr(1));
if (!/^([a-zA-Z]:)?\//.test(path))
return join(base, path);
return path;
}
function run(runner, options, name, callback) {
if (typeof name == "function") {
callback = name;
name = null;
}
if (!name)
name = "output";
options.relPath = options.path;
options.path = makeAbsolutePath(options.path);
options.path = c9.toExternalPath(options.path, "/");
options.cwd = makeAbsolutePath(options.cwd);
var proc = new Process(name, runner, options, callback);
processes.push(proc);
var event = { process: proc, runner: runner };
proc.on("starting", function() { handleEmit("starting", event); });
proc.on("started", function() { handleEmit("started", event); });
proc.on("stopping", function() { handleEmit("stopping", event); });
proc.on("stopped", function() {
handleEmit("stopped", event);
processes.remove(proc);
});
handleEmit("create", event);
return proc;
}
function stopAll() {
processes.forEach(function(proc) {
proc.stop();
});
}
/***** Process Class *****/
function Process(procName, runner, options, callback) {
var plugin = new Plugin("Ajax.org", main.consumes);
var emit = plugin.getEmitter();
emit.setMaxListeners(100);
var running = STOPPED;
var deferred = options && options.deferred;
var meta = {};
var pid, process, cmd;
if (typeof procName == "object") {
pid = procName.pid;
runner = procName.runner;
running = procName.running;
procName = procName.name;
}
// Deal with connection issues
c9.on("stateChange", function(e) {
if (e.state & c9.PROCESS) {
if (running == STARTED || running == STARTING)
checkState();
}
else if (running == STARTED || running == STARTING) {
emit("away");
}
}, plugin);
/***** Methods *****/
function run(srunner, options, callback) {
// If we're already running something do nothing
// @todo this check needs to be improved, to check the output buffer
if (running && (!options || !options.force))
return callback(new Error("Already running"));
running = STARTING;
if (srunner == "auto") {
return detectRunner(options, function(err, runner) {
if (err) return callback(err);
options.force = true;
run(runner, options, callback);
});
}
// Set the runner property
runner = srunner;
emit("starting");
if (deferred)
return setTimeout(callback);
var cmd = "";
// Display a message prior to running the command
if (runner.info)
cmd += "printf '\\033[1m" + runner.info.replace(/%/g, "%%") + "\\033[m\n' ; ";
// Set the PATH variable if needed
if (runner.path)
cmd += "export PATH=" + runner.path + " ; ";
var env = util.extend({}, options.env, runner.env);
for (var name in env) {
// HACK: old configurations used double quoting of environment values;
// let's support such nastiness for now
var value = /^["'].*["']$/.test(env[name])
? env[name]
: env[name].replace(/'/g, "'\\''");
cmd += "export " + name + "='" + value + "'; ";
}
// Open a pty session with tmux on the output buffer
if (runner.script) {
// Replace variables
cmd = insertVariables(cmd, options);
cmd += typeof runner.script == "string" ? runner.script : runner.script.join("\n");
var matches = cmd.match(/\$[\w\-]+/g) || [];
var seen = { args: true };
cmd = matches.map(function(key) {
if (seen[key])
return "";
seen[key] = 1;
var val = getVariable(key.slice(1), options);
if (val == key)
return "";
return key.slice(1) + "=" + bashQuote([val]) + ";";
}).join("") + "\n" + cmd;
} else {
// @todo add argument escaping
cmd += bashQuote(options.debug && runner["cmd-debug"] || runner.cmd);
// Replace variables
cmd = insertVariables(cmd, options);
}
var cwd = options.cwd || runner.working_dir
|| options.path && dirname(c9.toInternalPath(options.path)) || "/";
cwd = insertVariables(cwd, options);
console.log(cmd);
// Execute run.sh
proc.tmux(cmd, {
session: procName,
detach: options.detach !== false,
base: installPath.replace(/^~/, c9.home || "~"),
kill: true,
output: true,
cols: 100,
rows: 5,
cwd: cwd,
validatePath: true,
testing: testing
}, function(err, pty, processId) {
if (err)
return callback(err);
// Set process variable for later use
process = pty;
pid = processId;
// Running
running = STARTED;
emit("started", { pty: pty, options: options });
// if not detached
if (options.detach === false) {
// Hook data and exit events
pty.on("data", function(data) { emit("data", data); });
pty.on("exit", function() { emit("detach"); });
}
// Else if detached
else {
if (pid == -1) {
// The process already exited
callback(null, -1);
cleanup();
}
else {
callback(null, pid);
}
}
});
}
function getVariable(name, options) {
var fnme, idx;
var path = options.path;
var args = options.args;
var port;
if (name == "command")
return (options.relPath || "");
if (name == "file")
return (path || "");
if (name == "file_path")
return dirname(path || "");
if (name == "file_name")
return basename(path || "");
if (name == "file_extension") {
if (!path) return "";
fnme = basename(path);
idx = fnme.lastIndexOf(".");
return idx == -1 ? "" : fnme.substr(idx + 1);
}
if (name === "args")
return bashQuote(args, true);
if (name == "file_base_name") {
if (!path) return "";
fnme = basename(path);
idx = fnme.lastIndexOf(".");
return idx == -1 ? fnme : fnme.substr(0, idx);
}
if (name == "packages")
return installPath + "/packages";
if (name == "project_path")
return base;
if (name == "project_id")
return workspace.id;
if (name == "project_name")
return workspace.name;
if (name == "project_contents")
return workspace.contents;
if (name == "hostname")
return c9.hostname || "localhost";
if (name == "hostname_path") {
port = (options.local ? ":" + (c9.port || "8080") : "");
return ("https://" + c9.hostname || "http://localhost") + port + "/" + (options.relPath || "");
}
if (name == "url") {
port = (options.local ? ":" + (c9.port || "8080") : "");
return (c9.hostname
? "https://" + c9.hostname
: "http://localhost") + port;
}
if (name == "port")
return c9.port || "8080";
if (name == "ip")
return "0.0.0.0";
if (name == "home")
return c9.home;
if (name == "python")
return settings.get("project/python/@version");
if (name == "python_path")
return settings.get("project/python/@path");
if (name == "debug")
return options.debug + "";
return "$" + name;
}
function reverse(str) {
return str.split('').reverse().join('');
}
function insertVariables(cmd, options) {
// Loop until we get a fixpoint, since our pattern matches the
// two characters next to a variable and would otherwise skip
// over adjacent variables like $ip:$port.
var oldCmd;
do {
oldCmd = cmd;
cmd = cmd.replace(/(^|[^\\])(?:\$([\w_]+)|\$\{([^}]+)\})([^\\]|)/g,
function(m, startChar, name, nameBrackets, endChar) {
if (name || !nameBrackets)
return startChar + getVariable(name, options) + endChar;
else if (startChar) {
// Test for default value
if (nameBrackets.match(/^([\w_]+)\:(.*)$/))
return startChar + (getVariable(RegExp.$1, options) || RegExp.$2) + endChar;
// Test for conditional shell expression value
if (nameBrackets.match(/^([\w_]+)\?`(.*)`$/))
return options[RegExp.$1] ? "`" + RegExp.$2 + "`" : "";
// Test for conditional value
if (nameBrackets.match(/^([\w_]+)\?(.*)$/))
if (options[RegExp.$1])
return startChar + RegExp.$2 + endChar;
else if (startChar.trim().charAt(0).match(/['"]/))
return ""; // remove quotes
else
return startChar + endChar;
// Test for regular expression
if (nameBrackets.match(/^([\w_]+)\/(.*)$/)) {
return startChar + reverse(nameBrackets)
.replace(/^\/?(.*)\/(?!\\)(.*)\/(?!\\)([\w_]+)$/,
function (m, replace, find, name) {
var data = getVariable(reverse(name), options);
var re = new RegExp(reverse(find), "g");
return data.replace(re, reverse(replace));
}) + endChar;
}
// TODO quotes
// Assume just a name
return startChar + getVariable(nameBrackets, options) + endChar;
}
});
} while (cmd !== oldCmd);
return cmd;
}
function cleanup(callback) {
function finish() {
pid = 0;
runner = null;
running = STOPPED;
emit("stopped");
callback && callback();
return false; // Prevent error when watchfile doesn't exist
}
if (running == CLEANING || running == STOPPING || running == STOPPED) {
if (running !== 0)
return finish();
else
return callback && callback();
}
if (running > 0) {
running = STOPPING;
emit("stopping");
}
checkState();
}
function stop(callback) {
callback = callback || function() {};
if (!running)
return callback();
if (!pid) {
// If there's no PID yet, wait until we get one and then stop
if (running === STARTING) {
// Make sure the process times out
var timer = setTimeout(function() {
cleanup(function() {
callback(new Error("Could not get PID from process. "
+ "The process seemed to not be running anymore."));
});
}, 2000);
plugin.on("started", function(e) {
clearTimeout(timer);
if (e.pid > 0)
stop(callback);
else
callback();
});
}
else {
cleanup(function() {
callback(new Error("Could not get PID from running "
+ "process. Process might still be running in the "
+ "background."));
});
}
return;
}
running = STOPPING;
emit("stopping");
killOldProcess(callback);
}
function killOldProcess(callback) {
// (on Windows, execFile("kill") is handled specially)
if (c9.platform === "win32")
return proc.execFile("kill", { args: [pid]}, done);
var runCfg = runner;
if (runCfg && runCfg.cmdStop) {
return proc.execFile("bash", { args: ["-c", bashQuote(runCfg.cmdStop)]}, done);
}
proc.killtree(pid, { graceful: true }, function() {
if (runCfg && runCfg.cmdCleanup) {
proc.execFile("bash", { args: ["-c", bashQuote(runCfg.cmdCleanup)]}, done);
}
else if (meta.debug && runner && runner.debugport) {
var kill = "kill -9 $(lsof -i:" + runner.debugport + " -t);"
+ "if sudo -n true; then sudo kill -9 $(sudo lsof -i:" + runner.debugport + " -t); fi";
proc.execFile("sh", { args: ["-c", kill]}, done);
}
else {
done();
}
});
function done(err, e) {
// Clean up here to make sure runner is in correct state
if (err && err.code !== "EDISCONNECT")
err = null;
cleanup(function() {
callback(err, e);
});
}
}
var checking;
function checkState(cb) {
if (checking) return checking.push(cb);
checking = [cb];
// Execute run.sh
proc.tmux("", {
session: procName,
fetchpid: true
}, function(err, pty, newPid) {
var callbacks = checking || [];
checking = false;
// Process has exited
if (err || newPid == -1 || !newPid) {
cleanup();
}
else {
pid = newPid;
if (!running) {
running = STARTED;
emit("started", { pty: pty, options: options });
}
emit("back");
}
callbacks.forEach(function(cb) { cb && cb(); });
});
}
function getState() {
return {
pid: pid,
name: procName,
running: running >= 0 ? running : 0,
runner: runner
};
}
function detach(callback) {
// Kill the pty session
if (process)
process.write(String.fromCharCode(2) + "d");
// proc.execFile(TMUX, {
// args: [ "detach-client", "-t", procName ]
// }, callback);
}
/***** Register and define API *****/
/**
* Represents a process started with a `runner`.
*
* See the {@link run#run run plugin} for information on how to
* start a process.
*
* @class run.Process
*/
plugin.freezePublicAPI({
/**
* @property {-2} CLEANING Indicates the process run state is
* being cleaned up. To be tested against the `running` property.
*/
CLEANING: CLEANING,
/**
* @property {-1} STOPPING Indicates the process is being
* killed. To be tested against the `running` property.
*/
STOPPING: STOPPING,
/**
* @property {0} STOPPED Indicates the process is not running.
* To be tested against the `running` property.
*/
STOPPED: STOPPED,
/**
* @property {1} STARTING Indicates the process is getting
* started. To be tested against the `running` property.
*/
STARTING: STARTING,
/**
* @property {2} STARTED Indicates the process is running.
* To be tested against the `running` property.
*/
STARTED: STARTED,
/**
* @property {Number} running Indicates the state of the process.
*/
get running() { return running; },
/**
* @property {Object} runner The object describing how to run
* the process.
*/
get runner() { return runner; },
/**
* @property {Number} pid The pid of the running process if any
*/
get pid() { return pid; },
/**
* @property {String} name The name of the process.
*/
get name() { return procName; },
/**
* @property {Object} meta
*/
get meta() { return meta; },
/**
* @property {Object} command The command that started this process
*/
get command() { return cmd; },
_events: [
/**
* Fires when the process is going to be killed
* @event stopping
*/
"stopping",
/**
* Fires when the process stopped running
* @event stopped
*/
"stopped",
/**
* Fires when the process is being started
* @event starting
*/
"starting",
/**
* Fires when the process is started. This event also fires
* during startup if there's a PID file present
* @event started
*/
"started"
],
/**
* Returns the state of this process for use later.
* @return {Object}
*/
getState: getState,
/**
* Validates whether the process is still running
*/
checkState: checkState,
/**
* Detach from the currently running process. This is only
* relevant if options.detach was set to false when starting
* the process.
*/
detach: detach,
/**
* Stop the currently running process.
* @param {Function} callback Called when the process is stopped
* @param {Error} callback.err The error object, if an error
* has occured.
*/
stop: stop,
/**
*
*/
run: function(callback) {
if (!deferred)
return callback(new Error("Cannot call run() on non-deferred process"));
deferred = false;
options.force = true;
run(runner, options, callback || function() {});
},
/**
* Fetch variables from a string. See the {@link run#run run method} for more info.
* @param {String} str
*/
insertVariables: function(str) {
return insertVariables(str, options);
}
});
if (!pid)
run(runner, options, callback || function() {});
else
checkState();
return plugin;
}
function bashQuote(commandArgs, alsoQuoteArgs) {
if (!commandArgs)
return "";
return commandArgs.map(function(part) {
if (part === "$args" && !alsoQuoteArgs)
return part;
return part.match(/^`.*`$/) // shell expression
? part
: "\"" + part.replace(/'/g, "'\"'") + "\"";
}).join(" ");
}
/***** Lifecycle *****/
handle.on("load", function() {
load();
});
handle.on("enable", function() {
});
handle.on("disable", function() {
});
handle.on("unload", function() {
loaded = false;
});
/***** Register and define API *****/
/**
* Runs arbitrary programs and code from within Cloud9 based on a
* runner.
*
* *NB.: If you just want to run a process in the background you should
* use {@link proc#spawn} or related methods. This runner is architected
* specifically for running (and debugging) processes in Cloud9 that the
* user monitors in an {@link output} pane.*
*
* Example:
*
* run.getRunner("node", false, function(err, runner) {
* if (err) throw err.message;
*
* var process = run.run(runner, {
* path: "/helloworld.js"
* }, function(err, pid) {
* if (err) throw err.message;
*
* console.log("The PID is ", pid);
* });
* });
*
* You can also ask for auto-detection of the runner based on the file
* extension:
*
* var process = run.run("auto", {
* path: "/helloworld.js"
* }, function(err, pid) {
* if (err) throw err.message;
*
* console.log("The PID is ", pid);
* });
*
* A runner is a simple struct that describes how to run a
* certain subset of files. For instance a runner describing how to run
* Node.js files looks like this:
*
* {
* "cmd": [node, "${debug?--debug-brk=15454}", "$file"],
* "debugger": "v8",
* "debugport": 15454,
* "selector": "source.js",
* "info": "Your code is running at $hostname"
* }
*
* The concept of runners is based on the
* [Sublime Text(tm) Build Systems](http://docs.sublimetext.info/en/sublime-text-3/file_processing/build_systems.html),
* and is compatible with that format. There are a several
* built-in runners, and external plugins can add new runners as well.
* Users can also add runners to their .c9/runners directory in
* the workspace. We recommend users to commit these runners to their
* repository.
*
* The {@link build Cloud9 Build System} also uses a compatible
* format for the cloud9 builders.
*
* It is possible to combine builders and runners, therefore it is
* often not needed to describe the build and run step in the same
* definition.
*
* A process is always started in a [TMUX](http://en.wikipedia.org/wiki/Tmux)
* session. TMUX is a PTY multi-plexer which has several advantages;
* multiple clients can connect to the same session and the sessions are
* kept even if no user is connected.
*
* You can connect an {@link output} pane to the started process to
* see the output of your running process. The name passed to
* {@link run#run} should be the same as the name of the output pane
* you open:
*
* tabManager.open({
* editorType : "output",
* active : true,
* document : {
* title : "My Process Name",
* output : {
* id : "name_of_process"
* }
* }
* }, function(){});
*
* Note that by default the process name is "output" and is shown in the
* default output panel (available via the View menu).
*
* @singleton
*/
handle.freezePublicAPI({
/**
* @property {-2} CLEANING Indicates the process run state is
* being cleaned up. To be tested against the `runner` property.
*/
CLEANING: CLEANING,
/**
* Indicates the process is being killed. To be tested against
* the `running` property.
* @property {-1} STOPPING
*/
STOPPING: STOPPING,
/**
* Indicates the process is not running. To be tested against
* the `running` property.
* @property {0} STOPPED
*/
STOPPED: STOPPED,
/**
* Indicates the process is getting started. To be tested against
* the `running` property.
* @property {1} STARTING
*/
STARTING: STARTING,
/**
* Indicates the process is running. To be tested against
* the `running` property.
* @property {2} STARTED
*/
STARTED: STARTED,
/**
* @property {run.Process[]} processes List of running processes
*/
get processes() { return processes; },
/**
* @property {Object[]} runners List of available runners
*/
get runners() { return runners; },
_events: [
/**
* Fires when the process is going to be killed
* @event stopping
* @param {Object} e
* @param {run.Process} e.process the process that is stopping
*/
"stopping",
/**
* Fires when the process stopped running
* @event stopped
* @param {Object} e
* @param {run.Process} e.process the process that is stopped
*/
"stopped",
/**
* Fires when the process is being started
* @event starting
* @param {Object} e
* @param {run.Process} e.process the process that is starting
*/
"starting",
/**
* Fires when the process is started. This event also fires
* during startup if there's a PID file present
* @event started
* @param {Object} e
* @param {run.Process} e.process the process that is stopped
*/
"started",
/**
* Fires when the process is created.
* @event create
* @param {Object} e
* @param {run.Process} e.process the process that is created
*/
"create"
],
/**
* Retrieves an array of names of runners available to the system.
* A runner is a JSON file that describes how a certain file can
* be executed. The JSON file format is based on and compatible with
* the sublime build scripts. Besides the build in runners, the
* user can store runners in <installPath>/runners. This list will contain
* both the user's runners as well as the build-in runners.
* @param {Function} callback Called when the runners are retrieved
* @param {Error} callback.err The error object if an error occurred.
* @param {String[]} callback.runners A list of names of runners.
*/
listRunners: listRunners,
/**
* Detect the runner that will be used to run a certain file
* @param {Object} options
* @param {Function} callback
*/
detectRunner: detectRunner,
/**
* Retrieves an individual runner's JSON object based on it's name.
* The names of available runners can be retrieved using `listRunners`.
* @param {Function} callback Called when the runner is retrieved
* @param {Function} callback.err The error object if an error occurred.
* @param {Function} callback.runner A runner object. See {@link #run} for more information.
*/
getRunner: getRunner,
/**
* Adds a new runner to the list of runners
* @param {String} name The name of the runner to add
* @param {Object} runner The runner to add
*/
addRunner: addRunner,
/**
* Stop all running processes
*/
stopAll: stopAll,
/**
* Check whether a selector matches a certain path
* @param {String/Array} selector
* @param {String} path
*/
matchSelector: matchSelector,
/**
* Gets a process based on a pid
* @param {Object} state The state object returned by {@link run.Process.getState}.
* @return {run.Process}
*/
restoreProcess: restoreProcess,
/**
* Starts a process based on a runner and options that are passed.
* The runner can specify how to run a file. The implementation is
* based on sublime's build scripts. I'm copying some of their
* documentation here below for now:
* [Source: http://docs.sublimetext.info/en/latest/reference/build_systems.html]
*
* Generated commands can contain variables that are replaced just
* prior to running the command. The following list are the supported
* variables:
*
* <table>
* <tr><td>Variable</td><td> Description</td></tr>
* <tr><td>"$file_path"</td><td> The directory of the current file, e. g., C:\Files.</td></tr>
* <tr><td>"$file"</td><td> The full path to the current file, e. g., C:\Files\Chapter1.txt.</td></tr>
* <tr><td>"$args"</td><td> Any arguments entered after the file name.</td></tr>
* <tr><td>"$file_name"</td><td> The name portion of the current file, e. g., Chapter1.txt.</td></tr>
* <tr><td>"$file_extension"</td><td> The extension portion of the current file, e. g., txt.</td></tr>
* <tr><td>"$file_base_name"</td><td> The name only portion of the current file, e. g., Document.</td></tr>
* <tr><td>"$packages"</td><td> The full path to the Packages folder.</td></tr>
* <tr><td>"$project"</td><td> The full path to the current project file.</td></tr>
* <tr><td>"$project_path"</td><td> The directory of the current project file.</td></tr>
* <tr><td>"$project_name"</td><td> The name portion of the current project file.</td></tr>
* <tr><td>"$project_extension"</td><td> The extension portion of the current project file.</td></tr>
* <tr><td>"$project_base_name"</td><td> The name only portion of the current project file.</td></tr>
* <tr><td>"$hostname"</td><td> The hostname of the workspace.</td></tr>
* <tr><td>"$hostname_path"</td><td> The hostname of the workspace together with the relative path of the project file.</td></tr>
* <tr><td>"$url"</td><td> The full url to access the workspace.</td></tr>
* <tr><td>"$port"</td><td> The port assigned to the workspace.</td></tr>
* <tr><td>"$ip"</td><td> The ip address to run a process against in the workspace.</td></tr>
* </table>
*
* The following declarations can be used to add defaults or regexp
* replacements to the these variables:
*
* ${debug?--debug}
*
* This will emit --debug if the debug option is set to true
*
* ${project_name:Default}
*
* This will emit the name of the current project if there is one, otherwise Default.
*
* ${file/\.php/\.txt/}
*
* This will emit the full path of the current file, replacing .php with .txt.
*
* @param {Object/"auto"} runner Object describing how to run a process.
* Alternatively this can be set to "auto" to auto-detect the runner.
* @param {Array} runner.cmd Array containing the command to run and its desired
* arguments. If you dont specify an absolute path, the
* external program will be searched in your PATH, one of your
* systems environmental variables. The command can contain
* variables.
* @param {RegExp} [runner.line_regex] If file_regex doesnt match on the
* current line, but line_regex exists, and it does match on
* the current line, then walk backwards through the buffer
* until a line matching file regex is found, and use these two
* matches to determine the file and line to go to.
* @param {RegExp} [runner.selector] Used when the automatic selection of the
* runner is set. Cloud9 uses this scope selector to
* find the appropriate build system for the active view.
* @param {String} [runner.working_dir] Directory to change the current
* directory to before running cmd. The original current
* directory is restored afterwards.
* @param {Object} [runner.env] Dictionary of environment variables to be merged
* with the current process before passing them to cmd.
*
* Use this element, for example, to add or modify environment
* variables without modifying your systems settings.
* @param {Boolean} [runner.shell] If true, cmd will be run through the shell.
* In our implementation all commands run through the shell.
* This cannot be changed.
* @param {Boolean} [runner.debugger] Set this to the type string of
* the debugger that should connect to the process. The built-in
* debuggers are 'v8' and 'gdb'.
* @param {Boolean} [runner.debugport] Set this to the port number
* that the debugger will connect to.
* @param {String} [runner.path] This string will replace the current process
* PATH before calling cmd. The old PATH value will be restored
* after that.
*
* Use this option to add directories to PATH without having
* to modify your systems settings.
* @param {String} [runner.info] message to be outputted in the output buffer
* prior to running the processes. This message can contain
* variables.
* @param {Array} [runner.variants] currently not supported.
* @param {Object} options
* @param {String} options.path the path to the file to execute
* @param {String} options.cwd the current working directory
* @param {Array} options.args arguments to be passed to the program
* @param {Boolean} options.debug whether to start the process in debug mode
* @param {String} [name] the unique name of the output buffer.
* Defaults to "output". There can only be one process running on
* an output buffer at the same time. After a process has ended
* the process object is stale.
* @param {Function} callback called when the process is started
* @param {Error} callback.err The error object if an error occurred.
* @returns {run.Process} The process object
*/
run: run
});
register(null, {
run: handle
});
}
});