c9-core/node_modules/vfs-local/localfs.js

2490 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
// TODO investigate if using capture pane after desync is faster
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(""));
}