kopia lustrzana https://github.com/c9/core
2491 wiersze
87 KiB
JavaScript
2491 wiersze
87 KiB
JavaScript
var fs = require("fs");
|
|
var net = require("net");
|
|
var childProcess = require("child_process");
|
|
var constants = require("constants");
|
|
var join = require("path").join;
|
|
var pathResolve = require("path").resolve;
|
|
var pathNormalize = require("path").normalize;
|
|
var pathBasename = require("path").basename;
|
|
var dirname = require("path").dirname;
|
|
var basename = require("path").basename;
|
|
var Stream = require("stream").Stream;
|
|
var getMime = require("simple-mime")("application/octet-stream");
|
|
var vm = require("vm");
|
|
var exists = fs.exists || require("path").exists;
|
|
var crypto = require("crypto");
|
|
var os = require("os");
|
|
var pathSep = require("path").sep;
|
|
var transformPath;
|
|
var isWin = pathSep == "\\";
|
|
|
|
// node 0.6 does not have domain support
|
|
var domain;
|
|
try {
|
|
domain = require("domain");
|
|
} catch(e) {}
|
|
|
|
//////////////////// windows support ///////////////////////////////////////////
|
|
if (isWin) {
|
|
var _pathNormalize = pathNormalize;
|
|
var _join = join;
|
|
var _pathResolve = pathResolve;
|
|
pathNormalize = function(p) {
|
|
return _pathNormalize(p).replace(/[\\]/g, "/");
|
|
};
|
|
join = function() {
|
|
return _join.apply(null, arguments).replace(/[\\]/g, "/");
|
|
};
|
|
transformPath = function(path) {
|
|
if (path[0] == "/") {
|
|
var m = /^\/+(\w):[\/\\]*/.exec(path);
|
|
if (m) {
|
|
var device = m ? m[1] : "";
|
|
path = device + ":/" + path.substr(m[0].length).replace(/[:*?"<>|]/g, "_");
|
|
}
|
|
}
|
|
return path;
|
|
};
|
|
pathResolve = function(path, link) {
|
|
path = _pathResolve(path, link);
|
|
if (path[0] !== "/") {
|
|
path = "/" + path.replace(/[\\]/g, "/");
|
|
}
|
|
return path;
|
|
};
|
|
}
|
|
|
|
// For debugging only
|
|
function logToFile(message){
|
|
fs.appendFile('/tmp/vfs.log', new Date().getTime() + " " + message + "\n", function (err) {
|
|
|
|
});
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
module.exports = function setup(fsOptions) {
|
|
var pty;
|
|
if (fsOptions.nodePath) {
|
|
process.env.NODE_PATH = fsOptions.nodePath;
|
|
require("module")._initPaths();
|
|
}
|
|
if (!fsOptions.nopty) {
|
|
// on darwin trying to load binary for a wrong version crashes the process
|
|
[(fsOptions.nodePath || process.env.HOME + "/.c9/node_modules") + "/pty.js",
|
|
"pty.js", "pty.nw.js"].some(function(p) {
|
|
try {
|
|
pty = require(p);
|
|
return true;
|
|
} catch(e) {
|
|
console.warn(e, p);
|
|
}
|
|
});
|
|
if (!pty)
|
|
console.warn("unable to initialize pty.js:");
|
|
}
|
|
if (!pty) {
|
|
pty = function(command, options, callback) {
|
|
console.log("PTY is not supported.");
|
|
};
|
|
pty.spawn = pty;
|
|
}
|
|
|
|
var TMUX = fsOptions.tmuxBin || "tmux";
|
|
var BASH = fsOptions.bashBin || process.env.C9_BASH_BIN || (isWin ? "bash.exe" : "bash");
|
|
var METAPATH = fsOptions.metapath;
|
|
var WSMETAPATH = fsOptions.wsmetapath;
|
|
var TESTING = fsOptions.testing;
|
|
var TMUXNAME = "cloud9";
|
|
var noTmux;
|
|
var tmuxWarned;
|
|
|
|
// Check and configure options
|
|
var root = fsOptions.root;
|
|
if (!root) throw new Error("root is a required option");
|
|
root = pathNormalize(root);
|
|
|
|
if (pathSep == "/" && root[0] !== "/") throw new Error("root path must start in /");
|
|
if (root[root.length - 1] !== "/") root += "/";
|
|
|
|
var base = root.substr(0, root.length - 1);
|
|
|
|
// Fetch umask
|
|
var umask = 022;
|
|
_execFile(BASH, ["-c", "umask"], function(error, stdout, stderr) {
|
|
if (!error && !stderr && stdout)
|
|
umask = parseInt(stdout, 8);
|
|
});
|
|
|
|
// Fetch tmux version
|
|
_execFile(TMUX, ["-V"], function(err, stdout) {
|
|
if (err) stdout = "tmux 1.9";
|
|
noTmux = err && err.code === "ENOENT";
|
|
TMUXNAME = "cloud9" + parseFloat(stdout.replace(/tmux /, ""), 10);
|
|
});
|
|
|
|
if (fsOptions.hasOwnProperty('defaultEnv')) {
|
|
fsOptions.defaultEnv.__proto__ = process.env;
|
|
} else {
|
|
fsOptions.defaultEnv = process.env;
|
|
}
|
|
|
|
// Fetch environment variables from the login shell
|
|
var waitForEnv = null;
|
|
if (!isWin) {
|
|
waitForEnv = [];
|
|
_execFile(BASH, ["-lc", "printenv -0"], function(error, stdout, stderr) {
|
|
var pending = waitForEnv;
|
|
waitForEnv = null;
|
|
if (!error && !stderr && stdout) {
|
|
var env = fsOptions.defaultEnv;
|
|
stdout.split("\x00").forEach(function(entry) {
|
|
var i = entry.indexOf("=");
|
|
if (i != -1)
|
|
env[entry.slice(0, i)] = entry.slice(i + 1);
|
|
});
|
|
}
|
|
pending.forEach(function(x) { x(); });
|
|
});
|
|
}
|
|
|
|
// Storage for extension APIs
|
|
var apis = {};
|
|
// Storage for event handlers
|
|
var handlers = {};
|
|
|
|
var fileWatchers = {};
|
|
|
|
// Export the API
|
|
var vfs = wrapDomain({
|
|
// File management
|
|
resolve: resolve,
|
|
stat: stat,
|
|
readfile: readfile,
|
|
readdir: readdir,
|
|
mkfile: mkfile,
|
|
mkdir: mkdir,
|
|
mkdirP: mkdirP,
|
|
appendfile: appendfile,
|
|
rmfile: rmfile,
|
|
rmdir: rmdir,
|
|
rename: rename,
|
|
copy: copy,
|
|
symlink: symlink,
|
|
chmod: chmod,
|
|
|
|
// Set/Retrieve Metadata
|
|
metadata: metadata,
|
|
getMetadata: getMetadata,
|
|
|
|
// Wrapper around fs.watch or fs.watchFile
|
|
watch: watch,
|
|
|
|
// Network connection
|
|
connect: connect,
|
|
|
|
// Process Management
|
|
spawn: spawn,
|
|
pty: ptyspawn,
|
|
tmux: process.platform == "win32" ? bashspawn : tmuxspawn,
|
|
execFile: execFile,
|
|
killtree: killtree,
|
|
|
|
// Basic async event emitter style API
|
|
on: on,
|
|
off: off,
|
|
emit: emit,
|
|
|
|
// Extending the API
|
|
extend: extend,
|
|
unextend: unextend,
|
|
use: use
|
|
});
|
|
|
|
function wrapDomain(api) {
|
|
if (!domain)
|
|
return api;
|
|
|
|
for(var func in api) {
|
|
if (typeof api[func] !== "function")
|
|
continue;
|
|
|
|
(function(func) {
|
|
var call = api[func];
|
|
api[func] = function() {
|
|
var args = Array.prototype.slice.apply(arguments);
|
|
var d = domain.create();
|
|
d.on("error", function(e) {
|
|
console.error("VFS Exception in function '" + func + "':\n", (e.stack || e));
|
|
vfs.emit("error", {
|
|
message: e.message,
|
|
func: func,
|
|
stack: e.stack + "",
|
|
node: process.version
|
|
});
|
|
console.log("Scheduling process exit");
|
|
setTimeout(function() {
|
|
console.log("Exiting after uncaught exception in '" + func + "':\n", (e.stack || e))
|
|
process.exit(1);
|
|
}, 2000);
|
|
});
|
|
d.run(function() {
|
|
call.apply(api, args);
|
|
});
|
|
};
|
|
})(func);
|
|
}
|
|
return api;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
if (isWin) {
|
|
var readSpecialDir = function(path, callback) {
|
|
if (path == "/") {
|
|
execFile("cmd", { args: ["/c", "wmic logicaldisk get deviceid"] }, function(err, result) {
|
|
if (result && result.stdout) {
|
|
var drives = result.stdout.match(/ *\w:/g).map(function(x){return x.trim()});
|
|
var meta = {};
|
|
var stat = {
|
|
isFile: function() {return false},
|
|
size: 0,
|
|
mtime: Date.now()
|
|
};
|
|
// meta.notModified = true;
|
|
meta.etag = calcEtag(stat);
|
|
calcEtag(stat);
|
|
meta.stream = new Stream();
|
|
meta.stream.readable = true;
|
|
callback(null, meta);
|
|
drives.forEach(function(d) {
|
|
meta.stream.emit("data", {
|
|
mime: "drive/directory",
|
|
mtime: stat.mtime,
|
|
name: d,
|
|
size: 0
|
|
});
|
|
});
|
|
meta.stream.emit("end");
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
};
|
|
var isSpecialPath = function(path) {
|
|
return path == "/";
|
|
};
|
|
}
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function _execFile() {
|
|
var callback = arguments[arguments.length-1];
|
|
try {
|
|
return childProcess.execFile.apply(childProcess, arguments);
|
|
} catch(e) {
|
|
callback(e);
|
|
}
|
|
}
|
|
|
|
// Realpath a file and check for access
|
|
// callback(err, path)
|
|
function resolvePath(path, options, callback) {
|
|
if (!callback) {
|
|
callback = options;
|
|
options = {};
|
|
}
|
|
|
|
var alreadyRooted = options.alreadyRooted;
|
|
var checkSymlinks = options.checkSymlinks;
|
|
var sandbox = options.sandbox;
|
|
|
|
if (checkSymlinks === undefined)
|
|
checkSymlinks = true;
|
|
|
|
if (!alreadyRooted) {
|
|
if (sandbox)
|
|
path = join(sandbox, path);
|
|
|
|
path = join(root, path);
|
|
}
|
|
|
|
if (!options.nocheck) {
|
|
var localRoot = sandbox ? join(root, sandbox) : root;
|
|
var base = root.substr(0, localRoot.length - 1);
|
|
var testPath = path.substr(0, localRoot.length);
|
|
|
|
if (isWin) {
|
|
testPath = testPath.toLowerCase();
|
|
base = base.toLowerCase();
|
|
localRoot = localRoot.toLowerCase();
|
|
}
|
|
|
|
if (!(path === base || testPath === localRoot)) {
|
|
var isError = true;
|
|
|
|
if (isError) {
|
|
var err = new Error("EACCESS: '" + path + "' not in '" + localRoot + "'");
|
|
err.code = "EACCESS";
|
|
return callback(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (transformPath)
|
|
path = transformPath(path);
|
|
|
|
if ((checkSymlinks && fsOptions.checkSymlinks || checkSymlinks == 2) && !alreadyRooted)
|
|
fs.realpath(path, callback);
|
|
else
|
|
callback(null, path);
|
|
}
|
|
|
|
// A wrapper around fs.open that enforces permissions and gives extra data in
|
|
// the callback. (err, path, fd, stat)
|
|
function open(path, flags, mode, options, callback) {
|
|
resolvePath(path, options, function (err, path) {
|
|
if (err) return callback(err);
|
|
fs.open(path, flags, mode, function (err, fd) {
|
|
if (err) return callback(err);
|
|
fs.fstat(fd, function (err, stat) {
|
|
if (err) return callback(err);
|
|
callback(null, path, fd, stat);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// This helper function doesn't follow node conventions in the callback,
|
|
// there is no err, only entry.
|
|
function createStatEntry(file, fullpath, callback, _loop) {
|
|
fs.lstat(fullpath, function (err, stat) {
|
|
var entry = {
|
|
name: file
|
|
};
|
|
|
|
if (err) {
|
|
entry.err = err;
|
|
return callback(entry);
|
|
} else {
|
|
entry.size = stat.size;
|
|
entry.mtime = stat.mtime.valueOf();
|
|
|
|
if (stat.isDirectory()) {
|
|
entry.mime = "inode/directory";
|
|
} else if (stat.isBlockDevice()) entry.mime = "inode/blockdevice";
|
|
else if (stat.isCharacterDevice()) entry.mime = "inode/chardevice";
|
|
else if (stat.isSymbolicLink()) entry.mime = "inode/symlink";
|
|
else if (stat.isFIFO()) entry.mime = "inode/fifo";
|
|
else if (stat.isSocket()) entry.mime = "inode/socket";
|
|
else {
|
|
entry.mime = getMime(fullpath);
|
|
}
|
|
|
|
if (!stat.isSymbolicLink()) {
|
|
return callback(entry);
|
|
}
|
|
fs.readlink(fullpath, function (err, link) {
|
|
if (err) {
|
|
entry.linkErr = err.stack;
|
|
return callback(entry);
|
|
}
|
|
var fullLinkPath = pathResolve(dirname(fullpath), link);
|
|
if (!_loop) {
|
|
_loop = {fullLinkPath: fullpath, max: 100};
|
|
}
|
|
if (fullLinkPath.toLowerCase() == _loop.fullLinkPath.toLowerCase() || _loop.max --< 0) {
|
|
entry.linkErr = "ELOOP: recursive symlink";
|
|
return callback(entry);
|
|
}
|
|
entry.link = link;
|
|
resolvePath(fullLinkPath, {alreadyRooted: true}, function (err, newpath) {
|
|
if (err) {
|
|
entry.linkErr = err;
|
|
return callback(entry);
|
|
}
|
|
createStatEntry(basename(newpath), newpath, function (linkStat) {
|
|
entry.linkStat = linkStat;
|
|
linkStat.fullPath = newpath.substr(base.length) || "/";
|
|
return callback(entry);
|
|
}, _loop);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Common logic used by rmdir and rmfile
|
|
function remove(path, fn, options, callback) {
|
|
var meta = {};
|
|
resolvePath(path, options, function (err, realpath) {
|
|
if (err) return callback(err);
|
|
fn(realpath, function (err) {
|
|
if (err) return callback(err);
|
|
|
|
// Remove metadata
|
|
resolvePath(WSMETAPATH + path, options, function (err, realpath) {
|
|
if (err) return callback(null, meta);
|
|
|
|
fn(realpath, function(){
|
|
return callback(null, meta);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function resolve(path, options, callback) {
|
|
resolvePath(path, options, function (err, path) {
|
|
if (err) return callback(err);
|
|
callback(null, { path: path });
|
|
});
|
|
}
|
|
|
|
function stat(path, options, callback) {
|
|
// Make sure the parent directory is accessable
|
|
resolvePath(dirname(path), options, function (err, dir) {
|
|
if (err) return callback(err);
|
|
var file = basename(path);
|
|
path = join(dir, file);
|
|
createStatEntry(file, path, function (entry) {
|
|
if (entry.err) {
|
|
return callback(entry.err);
|
|
}
|
|
callback(null, entry);
|
|
});
|
|
});
|
|
}
|
|
|
|
function metadata(path, options, callback) {
|
|
if (path.charAt(0) == "~")
|
|
path = join(process.env.HOME, path.substr(1));
|
|
|
|
var dirpath = (path.substr(0,5) == "/_/_/"
|
|
? METAPATH + dirname(path.substr(4))
|
|
: WSMETAPATH + "/" + dirname(path));
|
|
|
|
resolvePath(dirpath, options, function (err, dir) {
|
|
if (err) return callback(err);
|
|
|
|
var file = basename(path);
|
|
path = join(dir, file);
|
|
if (pathSep === "\\")
|
|
dir = dir.replace(/\\/g, "/");
|
|
|
|
execFile("mkdir", { args: ["-p", dir] }, function(err){
|
|
if (err) return callback(err);
|
|
fs.writeFile(path, JSON.stringify(options.metadata), {}, function(err){
|
|
if (err) return callback(err);
|
|
callback(null, {});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function getMetadata(path, options, callback){
|
|
if (path.charAt(0) == "~")
|
|
path = join(process.env.HOME, path.substr(1));
|
|
|
|
var metaPath = join(WSMETAPATH, path);
|
|
|
|
resolvePath(metaPath, options, function (err, path) {
|
|
if (err) return callback(err);
|
|
fs.readFile(path, callback);
|
|
});
|
|
}
|
|
|
|
function readfile(path, options, callback) {
|
|
var meta = {};
|
|
var originalPath = path;
|
|
|
|
open(path, "r", 0666 & ~umask, options, function (err, path, fd, stat) {
|
|
if (err) return callback(err);
|
|
if (stat.isDirectory()) {
|
|
fs.close(fd);
|
|
err = new Error("EISDIR: Requested resource is a directory");
|
|
err.code = "EISDIR";
|
|
return callback(err);
|
|
}
|
|
|
|
// Basic file info
|
|
meta.mime = getMime(path);
|
|
meta.size = stat.size;
|
|
meta.etag = calcEtag(stat);
|
|
|
|
// ETag support
|
|
if ((TESTING || stat.mtime % 1000) && options.etag === meta.etag) {
|
|
meta.notModified = true;
|
|
fs.close(fd);
|
|
return callback(null, meta);
|
|
}
|
|
|
|
// Range support
|
|
if (options.hasOwnProperty('range') && !(options.range.etag && options.range.etag !== meta.etag)) {
|
|
var range = options.range;
|
|
var start, end;
|
|
if (range.hasOwnProperty("start")) {
|
|
start = range.start;
|
|
end = range.hasOwnProperty("end") ? range.end : meta.size - 1;
|
|
}
|
|
else {
|
|
if (range.hasOwnProperty("end")) {
|
|
start = meta.size - range.end;
|
|
end = meta.size - 1;
|
|
}
|
|
else {
|
|
meta.rangeNotSatisfiable = "Invalid Range";
|
|
fs.close(fd);
|
|
return callback(null, meta);
|
|
}
|
|
}
|
|
if (end < start || start < 0 || end >= stat.size) {
|
|
meta.rangeNotSatisfiable = "Range out of bounds";
|
|
fs.close(fd);
|
|
return callback(null, meta);
|
|
}
|
|
options.start = start;
|
|
options.end = end;
|
|
meta.size = end - start + 1;
|
|
meta.partialContent = { start: start, end: end, size: stat.size };
|
|
}
|
|
|
|
var metaData;
|
|
if (options.hasOwnProperty("metadata") && originalPath.indexOf(WSMETAPATH) == -1) {
|
|
getMetadata(originalPath, options, function (err, data) {
|
|
if (err)
|
|
return done();
|
|
try {
|
|
meta.metadataSize = data.length;
|
|
meta.metadataStringLength = data.toString("utf8").length;
|
|
metaData = data;
|
|
done(true);
|
|
} catch (e) {
|
|
fs.close(fd);
|
|
done();
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
done();
|
|
}
|
|
|
|
function done(fakeStream){
|
|
// HEAD request support
|
|
if (options.hasOwnProperty("head")) {
|
|
fs.close(fd);
|
|
return callback(null, meta);
|
|
}
|
|
|
|
// Read the file as a stream
|
|
try {
|
|
options.fd = fd;
|
|
meta.stream = new fs.ReadStream(path, options);
|
|
} catch (err) {
|
|
fs.close(fd);
|
|
return callback(err);
|
|
}
|
|
|
|
if (fakeStream) {
|
|
var readStream = meta.stream;
|
|
meta.stream = new Stream();
|
|
meta.stream.readable = true;
|
|
meta.stream.writable = true;
|
|
meta.stream.write = function (data) {
|
|
meta.stream.emit("data", data);
|
|
return true;
|
|
};
|
|
meta.stream.end = function (data) {
|
|
meta.stream.emit("end", data);
|
|
};
|
|
readStream.pipe(meta.stream, { end : false });
|
|
readStream.on("end", function(){
|
|
meta.stream.write(metaData);
|
|
meta.stream.end();
|
|
});
|
|
meta.stream.destroy = function () {
|
|
readStream.destroy();
|
|
};
|
|
}
|
|
|
|
callback(null, meta);
|
|
}
|
|
});
|
|
}
|
|
|
|
function readdir(path, options, callback) {
|
|
var meta = {};
|
|
|
|
resolvePath(path, options, function (err, path) {
|
|
if (err) return callback(err);
|
|
if (isWin && readSpecialDir) {
|
|
if (readSpecialDir(path, callback))
|
|
return;
|
|
}
|
|
fs.stat(path, function (err, stat) {
|
|
if (err) return callback(err);
|
|
if (!stat.isDirectory()) {
|
|
err = new Error("ENOTDIR: Requested resource is not a directory");
|
|
err.code = "ENOTDIR";
|
|
return callback(err);
|
|
}
|
|
|
|
// ETag support
|
|
meta.etag = calcEtag(stat);
|
|
if ((TESTING || stat.mtime % 1000) && options.etag === meta.etag) {
|
|
meta.notModified = true;
|
|
return callback(null, meta);
|
|
}
|
|
|
|
fs.readdir(path, function (err, files) {
|
|
if (err) return callback(err);
|
|
if (options.head) {
|
|
return callback(null, meta);
|
|
}
|
|
var stream = new Stream();
|
|
stream.readable = true;
|
|
var paused;
|
|
stream.pause = function () {
|
|
if (paused === true) return;
|
|
paused = true;
|
|
};
|
|
stream.resume = function () {
|
|
if (paused === false) return;
|
|
paused = false;
|
|
getNext();
|
|
};
|
|
meta.stream = stream;
|
|
callback(null, meta);
|
|
var index = 0;
|
|
stream.resume();
|
|
function getNext() {
|
|
if (index === files.length) return done();
|
|
var file = files[index++];
|
|
var fullpath = join(path, file);
|
|
|
|
createStatEntry(file, fullpath, function onStatEntry(entry) {
|
|
stream.emit("data", entry);
|
|
|
|
if (!paused) {
|
|
getNext();
|
|
}
|
|
});
|
|
}
|
|
function done() {
|
|
stream.emit("end");
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// This is used for creating / overwriting files. It always creates a new tmp
|
|
// file and then renames to the final destination.
|
|
// It will copy the properties of the existing file is there is one.
|
|
function mkfile(path, options, realCallback) {
|
|
var meta = {};
|
|
var called;
|
|
var callback = function (err) {
|
|
if (called) {
|
|
if (err) {
|
|
if (meta.stream) meta.stream.emit("error", err);
|
|
else console.error(err.stack);
|
|
}
|
|
else if (meta.stream) meta.stream.emit("saved");
|
|
return;
|
|
}
|
|
called = true;
|
|
return realCallback(err, meta);
|
|
};
|
|
|
|
if (options.stream && !options.stream.readable) {
|
|
return callback(new TypeError("options.stream must be readable."));
|
|
}
|
|
|
|
// Pause the input for now since we're not ready to write quite yet
|
|
var readable = options.stream;
|
|
if (readable) {
|
|
if (readable.pause) readable.pause();
|
|
var buffer = [];
|
|
readable.on("data", onData);
|
|
readable.on("end", onEnd);
|
|
}
|
|
|
|
var tempPath;
|
|
var resolvedPath = "";
|
|
var mode = options.mode || 0666 & ~umask;
|
|
|
|
start();
|
|
|
|
function onData(chunk) {
|
|
buffer.push(["data", chunk]);
|
|
}
|
|
function onEnd() {
|
|
buffer.push(["end"]);
|
|
}
|
|
function error(err) {
|
|
if (!options.bufferWrite)
|
|
resume();
|
|
if (tempPath) {
|
|
fs.unlink(tempPath, callback.bind(null, err));
|
|
}
|
|
else
|
|
return callback(err);
|
|
}
|
|
|
|
function resume() {
|
|
if (readable) {
|
|
// Stop buffering events and playback anything that happened.
|
|
readable.removeListener("data", onData);
|
|
readable.removeListener("end", onEnd);
|
|
|
|
buffer.forEach(function (event) {
|
|
readable.emit.apply(readable, event);
|
|
});
|
|
// Resume the input stream if possible
|
|
if (readable.resume) readable.resume();
|
|
}
|
|
}
|
|
|
|
function start() {
|
|
if (options.parents) {
|
|
mkdirP(dirname(path), options, function(err) {
|
|
if (err) return error(err);
|
|
resolve();
|
|
});
|
|
}
|
|
else {
|
|
resolve();
|
|
}
|
|
}
|
|
|
|
// Make sure the user has access to the directory and get the real path.
|
|
function resolve() {
|
|
options.checkSymlinks = 2;
|
|
resolvePath(path, options, function (err, _resolvedPath) {
|
|
if (err) {
|
|
if (err.code !== "ENOENT") {
|
|
return error(err);
|
|
}
|
|
// If checkSymlinks is on we'll get an ENOENT when creating a new file.
|
|
// In that case, just resolve the parent path and go from there.
|
|
resolvePath(dirname(path), options, function (err, dir) {
|
|
if (err) return error(err);
|
|
resolvedPath = join(dir, basename(path));
|
|
createTempFile();
|
|
});
|
|
return;
|
|
}
|
|
|
|
resolvedPath = _resolvedPath;
|
|
createTempFile();
|
|
});
|
|
}
|
|
|
|
function createTempFile() {
|
|
// Buffer in memory when the bufferWrite option is set to true
|
|
if (options.bufferWrite)
|
|
return bufferAndWrite();
|
|
|
|
tempPath = tmpFile(dirname(resolvedPath), "." + basename(resolvedPath) + "-", "~");
|
|
|
|
fs.stat(resolvedPath, create);
|
|
|
|
function create(err, stat, isParent) {
|
|
if (err) {
|
|
if (err.code === "ENOENT")
|
|
return fs.stat(dirname(resolvedPath), function(err, stat){
|
|
create(err, stat, true);
|
|
});
|
|
return error(err);
|
|
}
|
|
|
|
var uid = process.getuid ? process.getuid() : 0;
|
|
var gid = process.getgid ? process.getgid() : 0;
|
|
|
|
if (stat) {
|
|
gid = stat.gid;
|
|
if (!isParent) {
|
|
mode = stat.mode & 0777;
|
|
uid = stat.uid;
|
|
}
|
|
}
|
|
|
|
// node 0.8.x adds a "wx" shortcut, but since it's not in 0.6.x we use the
|
|
// longhand here.
|
|
var flags = constants.O_CREAT | constants.O_WRONLY | constants.O_EXCL;
|
|
fs.open(tempPath, flags, mode, options, function (err, fd) {
|
|
if (err)
|
|
return pipe();
|
|
|
|
fchown(fd, uid, gid, function(err) {
|
|
fs.close(fd);
|
|
if (err) {
|
|
fs.unlink(tempPath);
|
|
return pipe();
|
|
}
|
|
|
|
pipe(new fs.WriteStream(tempPath, {
|
|
encoding: options.encoding || null,
|
|
mode: mode
|
|
}));
|
|
});
|
|
});
|
|
|
|
function fchown(fd, uid, gid, callback) {
|
|
fs.fstat(fd, function (err, stat) {
|
|
if (err) return callback(err);
|
|
|
|
if (stat.uid == uid && stat.gid == gid)
|
|
return callback();
|
|
|
|
fs.fchown(fd, uid, gid, callback);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function bufferAndWrite(){
|
|
var buffers = [];
|
|
var hadError;
|
|
|
|
readable.on("data", function(chunk){
|
|
buffers.push(new Buffer(chunk));
|
|
});
|
|
|
|
readable.on("error", function(err){
|
|
hadError = err;
|
|
error(err);
|
|
});
|
|
|
|
readable.on("end", function(chunk){
|
|
if (hadError) return;
|
|
|
|
writeToWatchedFile(resolvedPath, function(afterWrite) {
|
|
fs.writeFile(resolvedPath, Buffer.concat(buffers), function(err) {
|
|
afterWrite(function() {
|
|
if (err) return error(err);
|
|
callback();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
resume();
|
|
}
|
|
|
|
function pipe(writable) {
|
|
var hadError;
|
|
var swap = true;
|
|
|
|
if (!writable) {
|
|
swap = false;
|
|
writable = new fs.WriteStream(resolvedPath, {
|
|
encoding: options.encoding || null,
|
|
mode: mode
|
|
});
|
|
}
|
|
|
|
if (readable) {
|
|
readable.pipe(writable);
|
|
}
|
|
else {
|
|
writable.on('open', function () {
|
|
if (hadError) return;
|
|
meta.stream = writable;
|
|
callback();
|
|
});
|
|
}
|
|
writable.on('error', function (err) {
|
|
hadError = true;
|
|
error(err);
|
|
});
|
|
|
|
// intercept the first close event and perform the swap
|
|
var emit = writable.emit;
|
|
writable.emit = function(name) {
|
|
var args = arguments;
|
|
|
|
if (name !== "close")
|
|
return emit.apply(writable, args);
|
|
|
|
writable.emit = emit;
|
|
|
|
if (!hadError) {
|
|
if (!swap) {
|
|
emit.apply(writable, args);
|
|
callback();
|
|
}
|
|
else {
|
|
writeToWatchedFile(resolvedPath, function(afterWrite) {
|
|
fs.rename(tempPath, resolvedPath, function(err) {
|
|
afterWrite(function() {
|
|
if (err) return error(err);
|
|
emit.apply(writable, args);
|
|
callback();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
emit.apply(writable, args);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
resume();
|
|
}
|
|
}
|
|
|
|
function mkdirP(path, options, callback) {
|
|
resolvePath(path, { checkSymlinks: false, sandbox: options.sandbox }, function(err, dir) {
|
|
if (err) return callback(err);
|
|
|
|
exists(dir, function(exists) {
|
|
if (exists) return callback(null, {});
|
|
if (pathSep === "\\")
|
|
dir = dir.replace(/\\/g, "/");
|
|
|
|
execFile("mkdir", { args: ["-p", dir] }, function(err) {
|
|
if (err && err.message.indexOf("exists") > -1)
|
|
callback({"code": "EEXIST", "message": err.message});
|
|
else
|
|
callback(null, {});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function mkdir(path, options, callback) {
|
|
var meta = {};
|
|
|
|
if (options.parents)
|
|
return mkdirP(path, options, callback);
|
|
|
|
// Make sure the user has access to the parent directory and get the real path.
|
|
resolvePath(dirname(path), options, function (err, dir) {
|
|
if (err) return callback(err);
|
|
path = join(dir, basename(path));
|
|
fs.mkdir(path, function (err) {
|
|
if (err) return callback(err);
|
|
callback(null, meta);
|
|
});
|
|
});
|
|
}
|
|
|
|
function appendfile(path, options, callback) {
|
|
resolvePath(path, options, function (err, resolvedPath) {
|
|
if (err) return callback(err);
|
|
fs.appendFile(resolvedPath, options.data, options, function (err) {
|
|
if (err) return callback(err);
|
|
callback(null, {});
|
|
});
|
|
});
|
|
}
|
|
|
|
function rmfile(path, options, callback) {
|
|
remove(path, fs.unlink, options, callback);
|
|
}
|
|
|
|
function rmdir(path, options, callback) {
|
|
if (options.recursive) {
|
|
remove(path, function(path, callback) {
|
|
execFile("rm", {args: ["-rf", path]}, callback);
|
|
}, options, callback);
|
|
}
|
|
else {
|
|
remove(path, fs.rmdir, options, callback);
|
|
}
|
|
}
|
|
|
|
function rename(path, options, callback) {
|
|
var from, to;
|
|
if (options.from) {
|
|
from = options.from; to = path;
|
|
}
|
|
else if (options.to) {
|
|
from = path; to = options.to;
|
|
}
|
|
else {
|
|
return callback(new Error("Must specify either options.from or options.to"));
|
|
}
|
|
var meta = {};
|
|
// Resolve path to source
|
|
resolvePath(from, options, function (err, frompath) {
|
|
if (err) return callback(err);
|
|
// Resolve path to target dir
|
|
resolvePath(dirname(to), options, function (err, dir) {
|
|
if (err) return callback(err);
|
|
var topath = join(dir, basename(to));
|
|
|
|
exists(topath, function(exists) {
|
|
// Determine if paths are the same on a case-insensitive file system
|
|
// (fs.realpath and fs.stat->ino don't help here)
|
|
var isSamePath = /darwin|^win/.test(os.platform())
|
|
&& frompath.toLowerCase() === topath.toLowerCase();
|
|
|
|
if (!exists || options.overwrite || isSamePath) {
|
|
// Rename the file
|
|
fs.rename(frompath, topath, function (err) {
|
|
if (err) return callback(err);
|
|
|
|
// Rename metadata
|
|
if (options.metadata !== false) {
|
|
var metaPath = WSMETAPATH;
|
|
rename(metaPath + from, {
|
|
to: metaPath + to,
|
|
metadata: false
|
|
}, function(err){
|
|
callback(null, meta);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
var err = new Error("File already exists.");
|
|
err.code = "EEXIST";
|
|
callback(err);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function copy(path, options, callback) {
|
|
var from, to;
|
|
if (options.from) {
|
|
from = options.from; to = path;
|
|
}
|
|
else if (options.to) {
|
|
from = path; to = options.to;
|
|
}
|
|
else {
|
|
return callback(new Error("Must specify either options.from or options.to"));
|
|
}
|
|
|
|
if (!options.overwrite) {
|
|
resolvePath(to, options, function(err, path){
|
|
if (err) {
|
|
if (err.code == "ENOENT")
|
|
return innerCopy(from, to);
|
|
|
|
return callback(err);
|
|
}
|
|
|
|
fs.stat(path, function(err, stat){
|
|
if (!err && stat && !stat.err) {
|
|
// TODO: this logic should be pushed into the application code
|
|
var path = to.replace(/(?:\.([\d]+))?(\.[^\.\/\\]*)?$/, function(m, d, e){
|
|
return "." + (parseInt(d, 10)+1 || 1) + (e ? e : "");
|
|
});
|
|
|
|
copy(from, {
|
|
to : path,
|
|
overwrite : false,
|
|
recursive : options.recursive,
|
|
sandbox : options.sandbox
|
|
}, callback);
|
|
}
|
|
else {
|
|
innerCopy(from, to);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
else {
|
|
innerCopy(from, to);
|
|
}
|
|
|
|
function innerCopy(from, to) {
|
|
if (options.recursive) {
|
|
resolvePath(from, options, function(err, rFrom){
|
|
resolvePath(to, options, function(err, rTo){
|
|
spawn("cp", {
|
|
args: [ "-a", rFrom, rTo ],
|
|
stdoutEncoding : "utf8",
|
|
stderrEncoding : "utf8",
|
|
stdinEncoding : "utf8"
|
|
}, function(err, child){
|
|
if (err) return callback(err);
|
|
|
|
var proc = child.process;
|
|
var hasError;
|
|
|
|
proc.stderr.on("data", function(d){
|
|
if (d) {
|
|
hasError = true;
|
|
callback(new Error(d));
|
|
}
|
|
});
|
|
proc.stdout.on("end", function() {
|
|
if (!hasError)
|
|
callback(null, { to: to, meta: null });
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
else {
|
|
readfile(from, { sandbox: options.sandbox }, function (err, meta) {
|
|
if (err) return callback(err);
|
|
mkfile(to, {stream: meta.stream, sandbox: options.sandbox}, function (err, meta) {
|
|
callback(err, {
|
|
to: to,
|
|
meta: meta
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function symlink(path, options, callback) {
|
|
if (!options.target) return callback(new Error("options.target is required"));
|
|
var meta = {};
|
|
// Get real path to target dir
|
|
resolvePath(dirname(path), options, function (err, dir) {
|
|
if (err) return callback(err);
|
|
path = join(dir, basename(path));
|
|
|
|
resolvePath(options.target, options, function (err, target) {
|
|
if (err) return callback(err);
|
|
fs.symlink(target, path, function (err) {
|
|
if (err) return callback(err);
|
|
callback(null, meta);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function WatcherWrapper(path, options, callback) {
|
|
var listeners = [];
|
|
var persistent = options.persistent;
|
|
var timer, isDir, watcher, _self = this;
|
|
|
|
function watch(callback) {
|
|
function sendError(e) {
|
|
if (callback)
|
|
return callback(e);
|
|
else {
|
|
throw new Error("File does not exist");
|
|
}
|
|
}
|
|
|
|
try {
|
|
removeFromList();
|
|
fileWatchers[path] = fileWatchers[path] || [];
|
|
fileWatchers[path].push(_self);
|
|
|
|
if (options.file) {
|
|
watcher = fs.watchFile(path, { persistent: false }, onWatchEvent);
|
|
watcher.close = function() {
|
|
removeFromList();
|
|
fs.unwatchFile(path);
|
|
};
|
|
}
|
|
else {
|
|
watcher = fs.watch(path, { persistent: false }, onWatchEvent);
|
|
var close = watcher.close.bind(watcher);
|
|
watcher.close = function() {
|
|
removeFromList();
|
|
close();
|
|
};
|
|
}
|
|
|
|
// without this deleting folder on windows was crashing server sometimes with EPERM error
|
|
watcher.on("error", function(e) {
|
|
console.error("[Watcher error]", e, path);
|
|
});
|
|
|
|
function removeFromList() {
|
|
if (fileWatchers[path]) {
|
|
fileWatchers[path] = fileWatchers[path].filter(function(w) {
|
|
return w !== _self;
|
|
});
|
|
if (!fileWatchers[path].length)
|
|
delete fileWatchers[path];
|
|
}
|
|
}
|
|
|
|
} catch(e) {
|
|
return sendError(e);
|
|
}
|
|
|
|
callback && callback(null, _self);
|
|
}
|
|
|
|
function close() {
|
|
watcher && watcher.close();
|
|
}
|
|
|
|
// Receives watch results, uses buffering
|
|
function onWatchEvent(event, filename) {
|
|
// No need to buffer if we can't expect more events
|
|
if (persistent === false)
|
|
return handleWatchEvent(event, filename);
|
|
|
|
// 350ms buffer to see if a new event comes in,
|
|
// and grace period where we don't rely on the watchers
|
|
clearTimeout(timer);
|
|
timer = setTimeout(function() {
|
|
handleWatchEvent(event, filename);
|
|
}, 350);
|
|
|
|
// Continue listening
|
|
// This timeout fixes an eternal loop that can occur with watchers
|
|
// But we should be save the next 350ms anyway per the above
|
|
if (event != "delete") {
|
|
close();
|
|
setTimeout(function() {
|
|
try {
|
|
watch();
|
|
} catch(e) {
|
|
if (e.code == "ENOENT") {
|
|
event = "delete";
|
|
sendToAllListeners(event, filename);
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
}, 15);
|
|
}
|
|
}
|
|
|
|
var handleWatchEvent = this.handleWatchEvent = function(event, filename, isVfsWrite) {
|
|
// it is a temp file
|
|
if (filename && filename.substr(-1) == "~"
|
|
&& filename.charAt(0) == ".")
|
|
return;
|
|
|
|
createStatEntry(pathBasename(path), path, function(entry) {
|
|
entry.vfsWrite = isVfsWrite || false;
|
|
|
|
if (entry.err) {
|
|
event = "delete";
|
|
close();
|
|
}
|
|
else if (isDir) {
|
|
event = "directory";
|
|
|
|
// This timeout helps when (for instance git) updates
|
|
// many files in a folder at the same time.
|
|
fs.readdir(path, function (err, files) {
|
|
if (err) {
|
|
event = "error";
|
|
return sendToAllListeners(event, filename, entry, err);
|
|
}
|
|
|
|
var latest, i = 0;
|
|
function statFiles() {
|
|
var file = files[i];
|
|
if (!file) return done();
|
|
|
|
var fullpath = join(path, file);
|
|
createStatEntry(file, fullpath, function(entry) {
|
|
files[i++] = entry;
|
|
|
|
if (!latest || entry.mtime > latest.mtime)
|
|
latest = entry;
|
|
|
|
statFiles();
|
|
});
|
|
}
|
|
|
|
function done() {
|
|
// Ignore if files is tmp file
|
|
if (latest && latest.name.substr(-1) == "~"
|
|
&& latest.name.charAt(0) == ".")
|
|
return;
|
|
|
|
sendToAllListeners(event, filename, entry, files);
|
|
}
|
|
|
|
statFiles();
|
|
});
|
|
return;
|
|
}
|
|
|
|
sendToAllListeners(event, filename, entry);
|
|
});
|
|
}
|
|
|
|
var sendToAllListeners = this.sendToAllListeners = function(event, filename, entry, files) {
|
|
listeners.forEach(function(fn) {
|
|
fn(event, filename, entry, files);
|
|
});
|
|
};
|
|
|
|
this.close = function(){
|
|
listeners = [];
|
|
if (watcher) {
|
|
watcher.removeListener("change", handleWatchEvent);
|
|
watcher.close();
|
|
}
|
|
};
|
|
|
|
this.on = function(name, fn){
|
|
if (name != "change")
|
|
watcher.on.apply(watcher, arguments);
|
|
else {
|
|
listeners.push(fn);
|
|
}
|
|
};
|
|
|
|
this.removeListener = function(name, fn){
|
|
if (name != "change")
|
|
watcher.removeListener.apply(watcher, arguments);
|
|
else {
|
|
listeners.splice(listeners.indexOf(fn), 1);
|
|
}
|
|
};
|
|
|
|
this.pause = function() {
|
|
close();
|
|
};
|
|
|
|
this.resume = function(callback) {
|
|
if (!listeners.length)
|
|
return callback();
|
|
watch(callback);
|
|
};
|
|
|
|
fs.stat(path, function (err, stat) {
|
|
if (err) {
|
|
callback(err);
|
|
return sendToAllListeners("delete");
|
|
}
|
|
|
|
if (isDir === undefined)
|
|
isDir = stat && stat.isDirectory();
|
|
|
|
watch(callback);
|
|
});
|
|
}
|
|
|
|
function watch(path, options, callback) {
|
|
resolvePath(path, options, function (err, path) {
|
|
if (isWin && isSpecialPath(path)) return callback(true);
|
|
if (err) return callback(err);
|
|
|
|
new WatcherWrapper(path, options, function(err, watcher){
|
|
if (err) return callback(err);
|
|
callback(null, { watcher: watcher });
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Write to a file that may be watched by a file watcher,
|
|
* making sure its file watching events are properly sent.
|
|
*
|
|
* @param {String} path Path of our file
|
|
* @param {Function} callback Function writing to path
|
|
* @param {Function} callback.afterWrite Function to call when done writing
|
|
* @param {Function} callback.afterWrite.callback Callback of afterWrite()
|
|
*/
|
|
function writeToWatchedFile(path, callback) {
|
|
if (!fileWatchers[path])
|
|
return callback(function(c) { c(); });
|
|
|
|
var watchers = fileWatchers[path].slice();
|
|
var parentDir = dirname(path) + "/";
|
|
var dirWatchers = (fileWatchers[parentDir] || []).slice();
|
|
|
|
watchers.forEach(function(w) {
|
|
w.pause();
|
|
});
|
|
callback(done);
|
|
|
|
function done(callback) {
|
|
if (!watchers.length)
|
|
return callback();
|
|
|
|
// Notify each watcher of changes and reactivate it
|
|
var watcher = watchers.pop();
|
|
fs.stat(path, function(err, stat) {
|
|
if (err || !stat) return;
|
|
stat.vfsWrite = true;
|
|
watcher.sendToAllListeners("change", basename(path), stat);
|
|
});
|
|
watcher.resume(function() {
|
|
done(callback);
|
|
});
|
|
}
|
|
}
|
|
|
|
function connect(port, options, callback) {
|
|
var retries = options.hasOwnProperty('retries') ? options.retries : 5;
|
|
var retryDelay = options.hasOwnProperty('retryDelay') ? options.retryDelay : 50;
|
|
tryConnect();
|
|
function tryConnect() {
|
|
var called = false;
|
|
var socket = net.connect(port, "127.0.0.1", function() {
|
|
if (called) return;
|
|
called = true;
|
|
|
|
if (options.hasOwnProperty('encoding')) {
|
|
socket.setEncoding(options.encoding);
|
|
}
|
|
callback(null, {stream:socket});
|
|
});
|
|
socket.once("error", function (err) {
|
|
if (err.code === "ECONNREFUSED" && retries) {
|
|
setTimeout(tryConnect, retryDelay);
|
|
retries--;
|
|
retryDelay *= 2;
|
|
return;
|
|
}
|
|
|
|
if (called) return;
|
|
called = true;
|
|
return callback(err);
|
|
});
|
|
}
|
|
}
|
|
|
|
function chmod(path, options, callback) {
|
|
resolvePath(path, options, function(err, path){
|
|
if (err) return callback(err);
|
|
|
|
_execFile("chmod", [options.mode, path], {},
|
|
function (err, stdout, stderr) {
|
|
if (err) {
|
|
err.stderr = stderr;
|
|
err.stdout = stdout;
|
|
return callback(err);
|
|
}
|
|
|
|
callback(null, {});
|
|
});
|
|
});
|
|
}
|
|
|
|
function spawn(executablePath, options, callback) {
|
|
if (waitForEnv)
|
|
return waitForEnv.push(spawn.bind(null, executablePath, options, callback));
|
|
|
|
var args = options.args || [];
|
|
|
|
_setDefaultEnv(options);
|
|
|
|
resolvePath(executablePath, {
|
|
nocheck : 1,
|
|
alreadyRooted : true
|
|
}, function(err, path){
|
|
if (err) return callback(err);
|
|
|
|
var child;
|
|
try {
|
|
child = childProcess.spawn(path, args, options);
|
|
} catch (err) {
|
|
return callback(err);
|
|
}
|
|
if (options.resumeStdin) child.stdin.resume();
|
|
if (options.hasOwnProperty('stdoutEncoding')) {
|
|
child.stdout && child.stdout.setEncoding(options.stdoutEncoding);
|
|
}
|
|
if (options.hasOwnProperty('stderrEncoding')) {
|
|
child.stderr && child.stderr.setEncoding(options.stderrEncoding);
|
|
}
|
|
|
|
// node 0.10.x emits error events if the file does not exist
|
|
child.on("error", function(err) {
|
|
child.emit("exit", 127);
|
|
});
|
|
|
|
callback(null, {
|
|
process: child
|
|
});
|
|
});
|
|
}
|
|
|
|
function ptyspawn(executablePath, options, callback) {
|
|
var args = options.args || [];
|
|
delete options.args;
|
|
|
|
_setDefaultEnv(options);
|
|
delete options.env.TMUX;
|
|
|
|
if (options.testing) {
|
|
args.forEach(function(arg, i){
|
|
args[i] = arg.replace(/^~/, process.env.HOME);
|
|
});
|
|
}
|
|
|
|
resolvePath(executablePath, {
|
|
nocheck : 1,
|
|
alreadyRooted : true
|
|
}, function(err, path){
|
|
if (err) return callback(err);
|
|
|
|
if (options.validatePath)
|
|
fs.exists(path, check);
|
|
else
|
|
check(true);
|
|
|
|
function check(exists){
|
|
if (!exists) {
|
|
var err = new Error("ENOENT: file not found " + path);
|
|
err.code = "ENOENT";
|
|
return callback(err);
|
|
}
|
|
|
|
var proc;
|
|
try {
|
|
proc = pty.spawn(path, args, options);
|
|
proc.on("error", function(){
|
|
// Prevent PTY from throwing an error;
|
|
// I don't know how to test and the src is funky because
|
|
// it tests for .length < 2. Who is setting the other event?
|
|
});
|
|
proc.resizeOrig = proc.resize;
|
|
proc.resize = function(cols, rows) {
|
|
try {
|
|
proc.resizeOrig(cols, rows);
|
|
} catch(e) {
|
|
console.error("error when resizing terminal", e);
|
|
return;
|
|
}
|
|
// todo add resize event
|
|
proc.emit("data", {rows: rows, cols: cols});
|
|
|
|
if (!tmuxWarned) {
|
|
if (/v0\.([123456789]\..*|10\.(0|1|2[0-7]))/.test(process.version)) {
|
|
proc.emit("data", {
|
|
message: "Wrong Node.js version: " + process.version,
|
|
code: "EINSTALL"
|
|
});
|
|
}
|
|
else if (TMUXNAME == "cloud91.6") {
|
|
proc.emit("data", {
|
|
message: "Wrong TMUX version: 1.6",
|
|
code: "EINSTALL"
|
|
});
|
|
}
|
|
else if (noTmux) {
|
|
proc.emit("data", {
|
|
message: "Please make sure TMUX is installed",
|
|
code: "EINSTALL"
|
|
});
|
|
}
|
|
tmuxWarned = true;
|
|
}
|
|
};
|
|
} catch (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
callback(null, {
|
|
pty: proc
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function escapeRegExp(str) {
|
|
return str.replace(/[-[\]{}()*+?.,\\^$|#\s"']/g, "\\$&");
|
|
}
|
|
|
|
/**
|
|
* @param {Boolean} [options.kill] First kill an existing session
|
|
* @param {Boolean} [options.attach] Attach if the session exists
|
|
* @param {Boolean} [options.detach] Detach immediately after starting the process. This will return a pid instead of a pty.
|
|
* @param {Boolean} [options.detachOthers] Detach other clients immediately after starting the process
|
|
* @param {Boolean} [options.fetchpid] Return the pid of the process started in the tmux session, or -1 if it's no longer running
|
|
* @param {Boolean} [options.output] Act like an output pane
|
|
* @param {Boolean} [options.base] The base path to store the watch files
|
|
*/
|
|
function tmuxspawn(ignored, options, callback) {
|
|
var tmuxName = options.tmuxName || TMUXNAME;
|
|
var session = options.session;
|
|
|
|
function fetchPid(callback, retries){
|
|
if (!retries) retries = 0;
|
|
_execFile(TMUX, [
|
|
"-u2", "-L", tmuxName, "-C",
|
|
"list-panes", "-F", "c9-pid-#{pane_pid}-#{pane_dead}-#{pane_status}",
|
|
"-t", session
|
|
], {
|
|
maxBuffer: 1000 * 1024
|
|
}, function(err, stdout){
|
|
var matches = /c9-pid-(\d+)-(\d)-/.exec(stdout);
|
|
var isDead = parseInt(matches && matches[2], 10);
|
|
var pid = isDead ? 0 : parseInt(matches && matches[1], 10);
|
|
|
|
if (!pid && !isDead && retries < 10) {
|
|
setTimeout(fetchPid.bind(null, callback, ++retries), 30);
|
|
return;
|
|
}
|
|
|
|
callback(err, {
|
|
pid: pid || -1
|
|
});
|
|
});
|
|
}
|
|
|
|
// Fetch PID of a running process and return it
|
|
if (options.fetchpid)
|
|
return fetchPid(callback);
|
|
|
|
// Capture the scrollback of a pane
|
|
if (options.capturePane) {
|
|
options = options.capturePane;
|
|
args = [
|
|
"-u2", // force utf and 256 color
|
|
"-L", tmuxName,
|
|
"capture-pane", options.joinLines !== false ? "-peJ" : "-pe",
|
|
"-S", options.start,
|
|
"-E", options.end,
|
|
"-t", options.pane
|
|
];
|
|
|
|
var child;
|
|
try {
|
|
child = childProcess.spawn(TMUX, args, options);
|
|
} catch (err) {
|
|
return callback(err);
|
|
}
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
|
|
// node 0.10.x emits error events if the file does not exist
|
|
child.on("error", function(err) {
|
|
child.emit("exit", 127);
|
|
});
|
|
|
|
callback(null, {
|
|
process: child
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (options.getStatus) {
|
|
options = options.getStatus;
|
|
var sessionId = options.id;
|
|
var args = ["-u2", "-L", tmuxName];
|
|
|
|
var paneFormat = {
|
|
"#S" : "session",
|
|
"#{pane_current_path}" : "path",
|
|
"#{pane_current_command}" : "command",
|
|
"#{pane_width}" : "width",
|
|
"#{pane_height}" : "height",
|
|
"#{history_limit}" : "length",
|
|
"#{history_size}" : "line",
|
|
"#{cursor_x}" : "x",
|
|
"#{cursor_y}" : "y",
|
|
"#{saved_cursor_x}" : "savedX",
|
|
"#{saved_cursor_y}" : "savedY",
|
|
"#{scroll_region_lower}" : "scrollRegionLower",
|
|
"#{scroll_region_upper}" : "scrollRegionUpper",
|
|
numberDataIndex : 3,
|
|
};
|
|
|
|
var clientFormat = {
|
|
"#{client_session}" : "session",
|
|
"#{client_created}" : "created",
|
|
"#{client_activity}" : "activity",
|
|
"#{client_width}" : "width",
|
|
"#{client_height}" : "height",
|
|
numberDataIndex : 1,
|
|
};
|
|
function getFormatString(map) {
|
|
return Object.keys(map).filter(function(x) {
|
|
return x[0] == "#";
|
|
}).join("\x01");
|
|
}
|
|
function getFormatObject(map, str) {
|
|
var data = str.split("\x01");
|
|
var result = {};
|
|
Object.keys(map).forEach(function(key, i) {
|
|
if (key[0] != "#") return;
|
|
var val = data[i];
|
|
if (i >= map.numberDataIndex)
|
|
val = parseInt(val, 10);
|
|
result[map[key]] = val;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
args.push("list-panes", "-F", getFormatString(paneFormat));
|
|
if (sessionId)
|
|
args.push("-t", sessionId);
|
|
else
|
|
args.push("-a");
|
|
|
|
if (options.listClients !== false) {
|
|
args.push(";", "list-clients", "-F", "\n" + getFormatString(clientFormat));
|
|
if (sessionId)
|
|
args.push("-t", sessionId);
|
|
}
|
|
|
|
return _execFile(TMUX, args, function(e, data) {
|
|
var panes = {};
|
|
var clientsSection = false;
|
|
(data || "").split("\n").forEach(function(str) {
|
|
if (!str)
|
|
return (clientsSection = true);
|
|
|
|
var pane;
|
|
if (clientsSection) {
|
|
var client = getFormatObject(clientFormat, str);
|
|
pane = panes[client.session];
|
|
if (pane)
|
|
pane.clients.push(client);
|
|
} else {
|
|
pane = getFormatObject(paneFormat, str);
|
|
panes[pane.session] = pane;
|
|
pane.clients = [];
|
|
}
|
|
});
|
|
// panes.raw = data;
|
|
if (sessionId)
|
|
panes = panes[sessionId];
|
|
callback(e, {status: panes});
|
|
});
|
|
}
|
|
|
|
// Kill the session with the same name before starting a new one
|
|
if (options.kill) {
|
|
if (!options.session)
|
|
return callback(new Error("Missing session name"));
|
|
|
|
// logToFile("Kill: " + options.session);
|
|
|
|
_execFile(TMUX,
|
|
["-L", tmuxName, "-C", "kill-session", "-t", options.session],
|
|
function(err){
|
|
if (!options.command)
|
|
return callback(err, {});
|
|
|
|
start();
|
|
});
|
|
}
|
|
// Attach to a session with the same name if it exists
|
|
else if (options.attach) {
|
|
if (!options.session)
|
|
return callback(new Error("Missing session name"));
|
|
|
|
(function findSession(retries){
|
|
_execFile(TMUX, ["-u2", "-L", tmuxName, "list-sessions"], function(err, stdout) {
|
|
if (err) stdout = ""; // This happens when the tmux server has not been started yet
|
|
|
|
var re = new RegExp("^" + escapeRegExp(options.session) + ":", "m");
|
|
if (stdout.match(re))
|
|
start(true);
|
|
else if (options.output && retries < 100) {
|
|
setTimeout(findSession.bind(null, ++retries), 10);
|
|
}
|
|
else {
|
|
// var error = new Error("Session doesn't exist: " + options.session);
|
|
// error.code = "ENOSESSIONFOUND";
|
|
// callback(error);
|
|
start(false);
|
|
}
|
|
});
|
|
})(0);
|
|
}
|
|
// Just start a new session. This will fail if a session with that name already exists
|
|
else
|
|
start();
|
|
|
|
function start(attach){
|
|
var args = [];
|
|
|
|
if (!options.env) options.env = {};
|
|
|
|
if (attach) {
|
|
// logToFile("Attach: " + options.session);
|
|
|
|
args = ["-u2", "-L", tmuxName, "attach", "-t", options.session];
|
|
if (options.detachOthers) {
|
|
// Work around https://github.com/chjj/pty.js/issues/68
|
|
if (/v0\.([123456789]\..*|10\.(0|1|2[0-7]))/.test(process.version))
|
|
console.log("detachOthers not supported, ignoring");
|
|
else
|
|
args.push("-d");
|
|
}
|
|
}
|
|
else {
|
|
// logToFile("New: " + options.session);
|
|
|
|
args = ["-u2", "-L", tmuxName, "new", "-s", options.session];
|
|
|
|
if (options.terminal) {
|
|
args.push("export ISOUTPUTPANE=0;"
|
|
+ (options.defaultEditor
|
|
? " export EDITOR='`which c9` open --wait'; "
|
|
: "")
|
|
+ BASH + " -l");
|
|
}
|
|
else if (options.idle) {
|
|
args.push(BASH + " -l -c 'printf \"\\e[01;34m[Idle]\\e[0m\\n\""
|
|
+ "; sleep 0.1;'");
|
|
}
|
|
else if (options.command) {
|
|
args.push(BASH + " -l -c '"
|
|
+ options.command.replace(/'/g, "'\\''")
|
|
+ '; printf "\\e[01;30m\\n\\nProcess exited with code: $?\\e[0m\\n"'
|
|
+ "; sleep 0.1;'");
|
|
}
|
|
|
|
args.push(
|
|
";", "set", "-q", "-g", "status", "off",
|
|
";", "set", "-q", "destroy-unattached", "off",
|
|
";", "set", "-q", "mouse-select-pane", "on",
|
|
";", "set", "-q", "set-titles", "on",
|
|
";", "set", "-q", "quiet", "on",
|
|
";", "set", "-q", "-g", "prefix", "C-b",
|
|
";", "set", "-q", "-g", "terminal-overrides", "'xterm:colors=256'"
|
|
);
|
|
|
|
// disable buffering of tmux output
|
|
// old versions of tmux skip parts of output without this flag
|
|
if (parseFloat(TMUXNAME.substr("cloud9".length)) < 2.1)
|
|
args.push(";", "setw", "c0-change-trigger", "0");
|
|
|
|
if (options.output) {
|
|
args.push(
|
|
";", "set", "-q", "remain-on-exit", "on",
|
|
";", "setw", "-q", "-g", "aggressive-resize", "on"
|
|
);
|
|
}
|
|
|
|
|
|
if (options.detach && options.output) {
|
|
args.unshift("-C");
|
|
args.push(";", "list-panes", "-F", "c9-pid#{pane_pid}-");
|
|
}
|
|
if (options.detach)
|
|
args.push(";", "detach");
|
|
|
|
options.env["LD_LIBRARY_PATH"] = (options.env["LD_LIBRARY_PATH"]
|
|
? options.env["LD_LIBRARY_PATH"] + ":" : "") + "~/.c9/local/lib";
|
|
|
|
// Prevent welcome message
|
|
options.env["ISOUTPUTPANE"] = "1";
|
|
}
|
|
|
|
run();
|
|
|
|
function run(err){
|
|
if (err) return callback(err);
|
|
|
|
if (options.detach && options.output) {
|
|
_setDefaultEnv(options);
|
|
delete options.env.TMUX;
|
|
|
|
return _execFile(TMUX, args, {
|
|
args: args,
|
|
name: options.name,
|
|
cwd: options.cwd,
|
|
resolve: options.resolve,
|
|
env: options.env
|
|
}, function(err, stdout) {
|
|
var m = /c9-pid(\d+)-/.exec(stdout);
|
|
var pid = parseInt(m && m[1], 10);
|
|
callback(err, {pid: pid});
|
|
});
|
|
}
|
|
|
|
ptyspawn(TMUX, {
|
|
args: args,
|
|
name: options.name,
|
|
cols: options.cols,
|
|
rows: options.rows,
|
|
cwd: options.cwd,
|
|
resolve: options.resolve,
|
|
env: options.env
|
|
}, function(err, meta){
|
|
if (err) {
|
|
logToFile("TMUX ERROR: " + err.message);
|
|
return callback(err);
|
|
}
|
|
|
|
if (!attach) {
|
|
meta.pty.on("data", function wait(data){
|
|
if (data)
|
|
meta.pty.removeListener("data", wait);
|
|
// Look for error states in plain text from tmux
|
|
if (data.indexOf("can't create socket") > -1) {
|
|
var err = new Error(data);
|
|
err.type = "exception";
|
|
err.code = "EPERM";
|
|
meta.pty.emit("data", err);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Return the pty
|
|
callback(null, meta);
|
|
|
|
if (attach)
|
|
meta.pty.emit("data", { started: true });
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function PtyStream(pty, isOutput, old){
|
|
if (old) {
|
|
return old.attachTo(pty, isOutput);
|
|
}
|
|
var exited = false;
|
|
var killed = false;
|
|
|
|
this.readable = true;
|
|
this.writable = true;
|
|
|
|
this.__defineGetter__("pid", function(){
|
|
return exited ? -1 : pty.pid;
|
|
});
|
|
|
|
function exit(){
|
|
if (exited) return;
|
|
|
|
exited = true;
|
|
emit("data", ["\n\x1b[1mPane is dead\x1b[H"]);
|
|
pty.kill();
|
|
pty.kill = function() {};
|
|
}
|
|
|
|
this.attachTo = function(newPty) {
|
|
pty = newPty;
|
|
exited = false;
|
|
killed = false;
|
|
|
|
this.readable = true;
|
|
this.writable = true;
|
|
|
|
Object.keys(events).forEach(forwardEvent);
|
|
return this;
|
|
}
|
|
|
|
this.killtree =
|
|
this.kill = isOutput ? function(signal){
|
|
// We dont want to really kill, just stop the process
|
|
if (signal == -1) {
|
|
if (exited)
|
|
emit("kill");
|
|
else {
|
|
exit();
|
|
|
|
pty.on("exit", function(){
|
|
emit("kill");
|
|
});
|
|
}
|
|
pty.suspended = true;
|
|
return;
|
|
}
|
|
|
|
killed = true;
|
|
|
|
// Otherwise we really kill this pty
|
|
emit("end");
|
|
emit("exit");
|
|
} : function(){
|
|
pty.kill();
|
|
// sometimes this can be called twice from worker and from options.kill
|
|
// pty.js doesn't like that
|
|
pty.kill = function() {};
|
|
};
|
|
|
|
this.destroy = function(){
|
|
pty.destroy.apply(pty, arguments);
|
|
};
|
|
|
|
this.end = function(){
|
|
pty.end.apply(pty, arguments);
|
|
};
|
|
|
|
this.write = function() {
|
|
pty.write.apply(pty, arguments);
|
|
};
|
|
|
|
// this.acknowledgeWrite = function(callback) {
|
|
// setTimeout(callback, 50); // 50ms time to ack input, per Winstein and Balakrishnan, 2013
|
|
// };
|
|
|
|
var events = {};
|
|
function forwardEvent(name){
|
|
events[name] = events[name] || [];
|
|
|
|
if (isOutput && (name == "exit" || name == "close" || name == "end")) {
|
|
if (name != "exit") return;
|
|
|
|
pty.on("exit", function(){
|
|
exit();
|
|
});
|
|
}
|
|
else {
|
|
pty.on(name, function(){
|
|
emit(name, arguments);
|
|
});
|
|
}
|
|
}
|
|
|
|
function emit(name, args){
|
|
if (!events[name])
|
|
return;
|
|
|
|
events[name].forEach(function(fn){ fn.apply(pty, args); });
|
|
}
|
|
|
|
this.on =
|
|
this.addListener = function(name, fn){
|
|
if (!events[name]) forwardEvent(name);
|
|
events[name].push(fn);
|
|
};
|
|
|
|
this.off =
|
|
this.removeListener = function(name, fn){
|
|
var idx = events[name].indexOf(fn);
|
|
if (idx > -1) events[name].splice(idx, 1);
|
|
};
|
|
|
|
this.emit = emit;
|
|
}
|
|
|
|
// Same as tmuxspawn but than spawns bash or other shell, for windows
|
|
var sessions = {};
|
|
function bashspawn(ignored, options, callback) {
|
|
var session;
|
|
|
|
function getSessionId(){
|
|
var id = "session" + Math.round(Math.random() * 1000);
|
|
return sessions[id] ? getSessionId() : id;
|
|
}
|
|
|
|
// Fetch PID of a running process and return it
|
|
if (options.fetchpid) {
|
|
session = sessions[options.session];
|
|
setTimeout(function() {
|
|
callback(null, { pid: session && session.pty ? session.pty.pid : -1 });
|
|
}, 100); // workaround for late exit message from winpty
|
|
return;
|
|
}
|
|
|
|
// Capture the scrollback of a pane
|
|
if (options.capturePane)
|
|
return callback(new Error("Not Supported on Windows"));
|
|
|
|
// Kill the session with the same name before starting a new one
|
|
if (options.kill) {
|
|
session = sessions[options.session];
|
|
if (session)
|
|
session.pty.kill();
|
|
|
|
if (!options.command)
|
|
return callback(session ? null : new Error("No Session Found"), {});
|
|
|
|
start();
|
|
}
|
|
// Attach to a session with the same name if it exists
|
|
else if (options.attach) {
|
|
if (!options.session)
|
|
return callback(new Error("Missing session name"));
|
|
|
|
session = sessions[options.session];
|
|
if (session) {
|
|
if (session.wait)
|
|
session.wait.push(callback);
|
|
else if (session.pty && !session.pty.suspended)
|
|
callback(null, { pty: session.pty });
|
|
}
|
|
else
|
|
start();
|
|
}
|
|
// Just start a new session. This will fail if a session with that name already exists
|
|
else {
|
|
if (options.session && sessions[options.session]) {
|
|
callback(new Error("Session Already Started"));
|
|
}
|
|
|
|
start();
|
|
}
|
|
|
|
function start() {
|
|
if (!options.session)
|
|
return callback(new Error("Missing session name"));
|
|
|
|
var args = ["-l", "-i"];
|
|
var name = options.session || getSessionId();
|
|
|
|
var session = sessions[name] || {};
|
|
sessions[name] = session;
|
|
if (!session.wait) session.wait = [];
|
|
|
|
if (options.idle) {
|
|
options.command = "echo '\033[2J\033[1;1H\033[01;34m[Idle]\033[0m'";
|
|
} else if (options.command) {
|
|
options.command = "echo '\033[2J\033[1;1H';" + options.command
|
|
+ ';printf "\033[01;30m\n\nProcess exited with code: $?\033[0m\n"';
|
|
}
|
|
|
|
if (options.command) {
|
|
args.push("-c", "nodosfilewarning=1;" + options.command);
|
|
|
|
var cmd = args[args.length - 1];
|
|
args[args.length - 1] = '"' + cmd.replace(/"/g, '\\"') + '"';
|
|
}
|
|
|
|
run();
|
|
|
|
function run(err){
|
|
if (err) return callback(err);
|
|
|
|
// Start PTY with TMUX
|
|
ptyspawn(options.BASH || BASH, {
|
|
args: args,
|
|
name: options.name,
|
|
cols: options.cols,
|
|
rows: options.rows,
|
|
cwd: options.cwd,
|
|
resolve: options.resolve,
|
|
env: options.env || {}
|
|
}, function(err, meta){
|
|
if (err) return callback(err);
|
|
|
|
session.pty = meta.pty =
|
|
new PtyStream(meta.pty, options.output, session.pty);
|
|
|
|
var wait = session.wait;
|
|
delete session.wait;
|
|
wait.forEach(function(cb){
|
|
cb(null, { pty: session.pty });
|
|
});
|
|
|
|
// Clear Session when pty ends
|
|
meta.pty.on("exit", function(){
|
|
delete sessions[name];
|
|
});
|
|
|
|
// Fetch the PID if appropriate
|
|
if (options.detach && options.output) {
|
|
session.pty.on("data", function wait(data){
|
|
// if (data.indexOf("aggressive-resize") > -1) {
|
|
|
|
session.pid = meta.pid = session.pty.pid;
|
|
callback(null, meta);
|
|
|
|
session.pty.removeListener("data", wait);
|
|
// }
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Return the pty
|
|
callback(null, meta);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function execFile(executablePath, options, callback) {
|
|
if (waitForEnv)
|
|
return waitForEnv.push(execFile.bind(null, executablePath, options, callback));
|
|
|
|
if (isWin && execFileWin(executablePath, options, callback))
|
|
return;
|
|
|
|
_setDefaultEnv(options);
|
|
|
|
resolvePath(executablePath, {
|
|
nocheck : 1,
|
|
alreadyRooted : true
|
|
}, function(err, path){
|
|
if (err) return callback(err);
|
|
|
|
_execFile(path, options.args || [],
|
|
options, function (err, stdout, stderr) {
|
|
if (err) {
|
|
err.stderr = stderr;
|
|
err.stdout = stdout;
|
|
return callback(err);
|
|
}
|
|
|
|
callback(null, {
|
|
stdout: stdout,
|
|
stderr: stderr
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function execFileWin(executablePath, options, callback) {
|
|
if (executablePath == "kill") {
|
|
var pid = options.args && options.args[0];
|
|
|
|
Object.keys(sessions).some(function(key) {
|
|
if (sessions[key].pid == pid && sessions[key].pty) {
|
|
sessions[key].pty.killtree(-1);
|
|
return true;
|
|
}
|
|
});
|
|
callback();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function _setDefaultEnv(options) {
|
|
if (options.hasOwnProperty("env"))
|
|
options.env.__proto__ = fsOptions.defaultEnv;
|
|
else
|
|
options.env = fsOptions.defaultEnv;
|
|
|
|
// Pty is only reading from the object itself;
|
|
var env = {};
|
|
for (var prop in options.env)
|
|
env[prop] = options.env[prop];
|
|
options.env = env;
|
|
|
|
if (options.cwd && options.cwd.charAt(0) == "~")
|
|
options.cwd = options.env.HOME + options.cwd.substr(1);
|
|
|
|
if (transformPath && options.cwd)
|
|
options.cwd = transformPath(options.cwd);
|
|
}
|
|
|
|
function killtree(pid, options, callback) {
|
|
var code = options.code || options.graceful ? "SIGTERM" : "SIGKILL";
|
|
|
|
childrenOfPid(pid, function killList(err, pidlist){
|
|
if (err)
|
|
return callback(err);
|
|
|
|
pidlist.forEach(function (pid) {
|
|
// if asked to kill ourselves do that only after killing all the children
|
|
if (pid == process.pid) {
|
|
return setTimeout(function() {
|
|
process.kill(pid, code);
|
|
});
|
|
}
|
|
try {
|
|
process.kill(pid, code);
|
|
} catch(e) {
|
|
if (e.code == "ESRCH")
|
|
return; // kill may throw if the pid does not exist.
|
|
// todo try killing with sudo in case of "EPERM"
|
|
}
|
|
});
|
|
if (options.graceful && code != "SIGKILL") {
|
|
code = "SIGKILL";
|
|
setTimeout(function() {
|
|
killList(null, pidlist);
|
|
}, options.timeout || 800);
|
|
} else {
|
|
callback(null, {});
|
|
}
|
|
});
|
|
}
|
|
|
|
function childrenOfPid(pid, callback) {
|
|
if (isWin)
|
|
return callback(null, [pid]);
|
|
|
|
_execFile("ps", ["-A", "-oppid,pid"], function(err, stdout, stderr) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
var parents = {};
|
|
stdout.split("\n").slice(1).forEach(function(line) {
|
|
var col = line.trim().split(/\s+/g);
|
|
(parents[col[0]] || (parents[col[0]] = [])).push(parseInt(col[1]));
|
|
});
|
|
|
|
function search(roots) {
|
|
var res = roots.concat();
|
|
for (var i = 0; i < roots.length; i++) {
|
|
var c = parents[roots[i]];
|
|
if (c) res.push.apply(res, search(c));
|
|
}
|
|
return res;
|
|
}
|
|
callback(null, search([pid]));
|
|
});
|
|
}
|
|
|
|
function on(name, handler, callback) {
|
|
if (!handlers[name]) handlers[name] = [];
|
|
handlers[name].push(handler);
|
|
callback && callback();
|
|
}
|
|
|
|
function off(name, handler, callback) {
|
|
var list = handlers[name];
|
|
if (list) {
|
|
var index = list.indexOf(handler);
|
|
if (index >= 0) {
|
|
list.splice(index, 1);
|
|
}
|
|
}
|
|
callback && callback();
|
|
}
|
|
|
|
function emit(name, value, callback) {
|
|
var list = handlers[name];
|
|
if (list) {
|
|
for (var i = 0, l = list.length; i < l; i++) {
|
|
list[i](value);
|
|
}
|
|
}
|
|
callback && callback();
|
|
}
|
|
|
|
function extend(name, options, callback) {
|
|
|
|
var meta = {};
|
|
// Pull from cache if it's already loaded.
|
|
if (!options.redefine && apis.hasOwnProperty(name)) {
|
|
var err = new Error("EEXIST: Extension API already defined for " + name);
|
|
err.code = "EEXIST";
|
|
return callback(err, { api: apis[name] });
|
|
}
|
|
|
|
if (options.redefine && apis[name] && apis[name].destroy)
|
|
apis[name].destroy();
|
|
|
|
var fn;
|
|
|
|
// The user can pass in a path to a file to require
|
|
if (options.file) {
|
|
try { fn = require(options.file); }
|
|
catch (err) { return callback(err); }
|
|
exec(fn);
|
|
}
|
|
|
|
// User can pass in code as a pre-buffered string
|
|
else if (options.code) {
|
|
try { fn = evaluate(options.code, name); }
|
|
catch (err) { return callback(err); }
|
|
exec(fn);
|
|
}
|
|
|
|
// Or they can provide a readable stream
|
|
else if (options.stream) {
|
|
consumeStream(options.stream, function (err, code) {
|
|
if (err) return callback(err);
|
|
var fn;
|
|
try {
|
|
fn = evaluate(code);
|
|
} catch(err) {
|
|
return callback(err);
|
|
}
|
|
exec(fn);
|
|
});
|
|
}
|
|
|
|
else {
|
|
return callback(new Error("must provide `file`, `code`, or `stream` when cache is empty for " + name));
|
|
}
|
|
|
|
function exec(fn) {
|
|
delete options.code;
|
|
delete options.stream;
|
|
delete options.file;
|
|
|
|
fn(vfs, options, function(err, exports) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
exports.names = Object.keys(exports);
|
|
exports.name = name;
|
|
|
|
wrapDomain(exports);
|
|
|
|
if (exports.on)
|
|
console.warn("Warning: " + name + " exports 'on' symbol that will be overwritten");
|
|
apis[name] = exports;
|
|
meta.api = exports;
|
|
callback(null, meta);
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
function unextend(name, options, callback) {
|
|
if (apis[name] && apis[name].destroy)
|
|
apis[name].destroy();
|
|
|
|
delete apis[name];
|
|
callback(null, {});
|
|
}
|
|
|
|
function use(name, options, callback) {
|
|
var api = apis[name];
|
|
if (!api) {
|
|
var err = new Error("ENOENT: There is no API extension named " + name);
|
|
err.code = "ENOENT";
|
|
return callback(err);
|
|
}
|
|
callback(null, {api:api});
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
if (fsOptions.extendApi) {
|
|
for (var i in fsOptions.extendApi) {
|
|
extend(i, fsOptions.extendApi[i], function() {});
|
|
}
|
|
}
|
|
|
|
return vfs;
|
|
|
|
};
|
|
|
|
// Consume all data in a readable stream and call callback with full buffer.
|
|
function consumeStream(stream, callback) {
|
|
var chunks = [];
|
|
stream.on("data", onData);
|
|
stream.on("end", onEnd);
|
|
stream.on("error", onError);
|
|
function onData(chunk) {
|
|
chunks.push(chunk);
|
|
}
|
|
function onEnd() {
|
|
cleanup();
|
|
callback(null, chunks.join(""));
|
|
}
|
|
function onError(err) {
|
|
cleanup();
|
|
callback(err);
|
|
}
|
|
function cleanup() {
|
|
stream.removeListener("data", onData);
|
|
stream.removeListener("end", onEnd);
|
|
stream.removeListener("error", onError);
|
|
}
|
|
}
|
|
|
|
// node-style eval
|
|
function evaluate(code, name) {
|
|
var exports = {};
|
|
var module = { exports: exports };
|
|
var fn = vm.runInThisContext(
|
|
"(function(require, exports, module, __dirname, __filename) {"
|
|
+ code
|
|
+ "})"
|
|
, name || "dynamic-" + Date.now().toString(36));
|
|
fn(require, exports, module, "", "");
|
|
return module.exports;
|
|
}
|
|
|
|
// Calculate a proper etag from a nodefs stat object
|
|
function calcEtag(stat) {
|
|
return (stat.isFile() ? '': 'W/') + '"' + (stat.ino || 0).toString(36) + "-" + stat.size.toString(36) + "-" + stat.mtime.valueOf().toString(36) + '"';
|
|
}
|
|
|
|
function uid(length) {
|
|
return (crypto
|
|
.randomBytes(length)
|
|
.toString("base64")
|
|
.slice(0, length)
|
|
.replace(/[+\/]+/g, "")
|
|
);
|
|
}
|
|
|
|
function tmpFile(baseDir, prefix, suffix) {
|
|
return join(baseDir, [prefix || "", uid(20), suffix || ""].join(""));
|
|
}
|