blob: d8b4f6a52391fb38a39e4299a0f39eb9a09114f6 [file] [log] [blame]
// Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* This is nassh as a lib.wam.fs.Executable.
*
* It's connected to the nassh filesystem in nassh_commands.js.
*/
nassh.Nassh = function(executeContext) {
// TODO(binji): some way to specify whether TTY should share input/output
// with stdin/stdout. We only want to separate the two when piping SSH output
// (e.g. scp)
this.streamManager_ = new nassh.StreamManager();
this.inputBuffer_ = new nassh.InputBuffer();
this.ttyInputBuffer_ = new nassh.InputBuffer();
this.executeContext = executeContext;
this.executeContext.stdin.onData.addListener(this.onStdIn_, this);
this.executeContext.ttyin.onData.addListener(this.onTTYIn_, this);
this.executeContext.onTTYChange.addListener(this.onTTYChange_, this);
this.executeContext.onClose.addListener(this.onExecuteClose_, this);
executeContext.ready();
executeContext.requestTTY({interrupt: ''});
var ecArg = executeContext.args.arg;
if (ecArg instanceof Array) {
ecArg = {argv: ecArg};
} else if (!(ecArg instanceof Object)) {
executeContext.closeError(new axiom.core.error.AxiomError.TypeMismatch(
'argv: Expected Array or Object', typeof ecArg));
return;
}
/**
* The argv object to pass to the NaCl plugin.
*/
this.argv = {};
if (ecArg.argv instanceof Array) {
this.argv.arguments = [].concat(ecArg.argv);
} else {
this.argv.arguments = [];
}
var washEnv = this.executeContext.getEnvs();
this.argv.environment = {};
for (var key in washEnv) {
if (key.substr(0, 1) == '$')
this.argv.environment[key.substr(1)] = washEnv[key];
}
var tty = executeContext.getTTY();
this.argv.terminalWidth = tty.columns;
this.argv.terminalHeight = tty.rows;
this.argv.writeWindow = 8 * 1024;
this.plugin_ = null;
this.onInit = new lib.Event();
this.initPlugin_(this.onInit);
};
/**
* Paths that can be opened by the plugin.
*/
nassh.Nassh.DEV_TTY = '/dev/tty';
nassh.Nassh.DEV_STDIN = '/dev/stdin';
nassh.Nassh.DEV_STDOUT = '/dev/stdout';
nassh.Nassh.DEV_STDERR = '/dev/stderr';
/**
* Invoked from nassh.Commands.on['nassh'].
*
* This is the entrypoint when invoked as an executable.
*/
nassh.Nassh.main = function(executeContext) {
var session = new nassh.Nassh(executeContext);
session.onInit.addListener(session.start.bind(session));
return executeContext.ephemeralPromise;
};
/**
* File descriptors used when talking to the plugin about stdio.
*/
nassh.Nassh.STDIN = 0;
nassh.Nassh.STDOUT = 1;
nassh.Nassh.STDERR = 2;
/**
* Perform final cleanup when it's time to exit this nassh session.
*/
nassh.Nassh.prototype.exit = function(name, arg) {
// TODO(binji): is this not called?
if (this.plugin_) {
this.plugin_.parentNode.removeChild(this.plugin_);
this.plugin_ = null;
}
};
/**
* Tell the NaCl plugin it's time to start.
*/
nassh.Nassh.prototype.start = function() {
this.sendToPlugin_('startSession', [this.argv]);
};
nassh.Nassh.prototype.print = function(str, opt_onAck) {
this.executeContext.stderr.write(str, opt_onAck);
};
nassh.Nassh.prototype.println = function(str, opt_onAck) {
this.executeContext.stderr.write(str + '\n');
};
nassh.Nassh.prototype.initPlugin_ = function(onComplete) {
this.print(nassh.msg('PLUGIN_LOADING'));
this.plugin_ = window.document.createElement('embed');
this.plugin_.setAttribute('src', '../plugin/pnacl/ssh_client.nmf');
this.plugin_.setAttribute('type', 'application/x-nacl');
this.plugin_.addEventListener('load', function() {
this.println(nassh.msg('PLUGIN_LOADING_COMPLETE'));
this.executeContext.stdin.resume();
this.executeContext.ttyin.resume();
setTimeout(this.onTTYChange_.bind(this));
onComplete();
}.bind(this));
this.plugin_.addEventListener('message', function(e) {
var msg = JSON.parse(e.data);
msg.argv = msg.arguments;
if (msg.name in this.onPlugin_) {
this.onPlugin_[msg.name].apply(this, msg.arguments);
} else {
console.log('Unknown message from plugin: ' + JSON.stringify(msg));
}
}.bind(this));
this.plugin_.addEventListener('crash', function(e) {
console.log('plugin crashed');
this.executeContext.closeError('wam.FileSystem.Error.PluginCrash',
[this.plugin_.exitStatus]);
}.bind(this));
document.body.insertBefore(this.plugin_, document.body.firstChild);
// Set mimetype twice for http://crbug.com/371059
this.plugin_.setAttribute('type', 'application/x-nacl');
};
/**
* Send a message to the nassh plugin.
*
* @param {string} name The name of the message to send.
* @param {Array} arguments The message arguments.
*/
nassh.Nassh.prototype.sendToPlugin_ = function(name, args) {
var str = JSON.stringify({name: name, arguments: args});
if (this.plugin_)
this.plugin_.postMessage(str);
};
nassh.Nassh.prototype.onExecuteClose_ = function(reason, value) {
if (this.plugin_) {
this.plugin_.parentNode.removeChild(this.plugin_);
this.plugin_ = null;
}
};
/**
* Hooked up to the onInput event of the message that started nassh.
*/
nassh.Nassh.prototype.onStdIn_ = function(value) {
if (typeof value != 'string')
return;
this.inputBuffer_.write(value);
};
/**
* Hooked up to the onInput event of the message that started nassh.
*/
nassh.Nassh.prototype.onTTYIn_ = function(value) {
if (typeof value != 'string')
return;
this.ttyInputBuffer_.write(value);
};
nassh.Nassh.prototype.onTTYChange_ = function() {
if (!this.plugin_)
return;
var tty = this.executeContext.getTTY();
this.sendToPlugin_('onResize', [Number(tty.columns), Number(tty.rows)]);
};
/**
* Plugin message handlers.
*/
nassh.Nassh.prototype.onPlugin_ = {};
/**
* Log a message from the plugin.
*/
nassh.Nassh.prototype.onPlugin_.printLog = function(str) {
console.log('plugin log: ' + str);
};
/**
* Plugin has exited.
*/
nassh.Nassh.prototype.onPlugin_.exit = function(code) {
console.log('plugin exit: ' + code);
this.sendToPlugin_('onExitAcknowledge', []);
this.executeContext.requestTTY({interrupt: null});
this.executeContext.closeOk(code);
};
/**
* Helper function to create a TTY stream.
*
* @param {integer} fd The file descriptor index.
* @param {boolean} allowRead True if this stream can be read from.
* @param {boolean} allowWrite True if this stream can be written to.
* @param {function} onOpen Callback to call when the stream is opened.
*
* @return {Object} The newly created stream.
*/
nassh.Nassh.prototype.createTtyStream = function(
fd, path, allowRead, allowWrite, onOpen) {
var cx = this.executeContext;
var arg = {
fd: fd,
allowRead: allowRead,
allowWrite: allowWrite,
};
if (path == nassh.Nassh.DEV_TTY) {
arg.inputBuffer = this.ttyInputBuffer_;
arg.onWrite = cx.ttyout.write.bind(cx.ttyout);
} else if (path == nassh.Nassh.DEV_STDIN) {
arg.inputBuffer = this.inputBuffer_;
} else if (path == nassh.Nassh.DEV_STDOUT) {
arg.onWrite = cx.stdout.write.bind(cx.stdout);
} else if (path == nassh.Nassh.DEV_STDERR) {
arg.onWrite = cx.stderr.write.bind(cx.stderr);
}
var streamClass = nassh.Stream.Tty;
var stream = this.streamManager_.openStream(streamClass, fd, arg, onOpen);
if (allowRead) {
var onDataAvailable = function(isAvailable) {
// Send current read status to plugin.
setTimeout(function() {
this.sendToPlugin_('onReadReady', [fd, isAvailable]);
}.bind(this), 0);
}.bind(this);
arg.inputBuffer.onDataAvailable.addListener(onDataAvailable);
stream.onClose = function(reason) {
arg.inputBuffer.onDataAvailable.removeListener(onDataAvailable);
this.sendToPlugin_('onClose', [fd, reason]);
}.bind(this);
}
return stream;
};
/**
* Plugin wants to open a file.
*
* The only supported paths are /dev/stdin, /dev/stdout, /dev/stderr and
* /dev/tty.
*/
nassh.Nassh.prototype.onPlugin_.openFile = function(fd, path, mode) {
// TODO(binji): this should be determined as being whether stdin/stdout are
// being piped.
var isAtty =
!(path == nassh.Nassh.DEV_STDIN || path == nassh.Nassh.DEV_STDOUT);
if (path == nassh.Nassh.DEV_TTY || path == nassh.Nassh.DEV_STDIN ||
path == nassh.Nassh.DEV_STDOUT || path == nassh.Nassh.DEV_STDERR) {
var allowRead =
path == nassh.Nassh.DEV_STDIN || path == nassh.Nassh.DEV_TTY;
var allowWrite = path == nassh.Nassh.DEV_STDOUT ||
path == nassh.Nassh.DEV_STDERR ||
path == nassh.Nassh.DEV_TTY;
var stream = this.createTtyStream(
fd, path, allowRead, allowWrite, function(success) {
this.sendToPlugin_('onOpenFile', [fd, success, isAtty]);
}.bind(this));
} else {
this.sendToPlugin_('onOpenFile', [fd, false, false]);
}
};
/**
* Plugin wants to write some data to a file descriptor.
*/
nassh.Nassh.prototype.onPlugin_.write = function(fd, data) {
var stream = this.streamManager_.getStreamByFd(fd);
if (!stream) {
console.warn('Attempt to write to unknown fd: ' + fd);
return;
}
stream.asyncWrite(data, function(writeCount) {
this.sendToPlugin_('onWriteAcknowledge', [fd, writeCount]);
}.bind(this), 100);
};
/**
* Plugin wants to read from a file descriptor.
*/
nassh.Nassh.prototype.onPlugin_.read = function(fd, size) {
var stream = this.streamManager_.getStreamByFd(fd);
if (!stream) {
console.warn('Attempt to read from unknown fd: ' + fd);
return;
}
stream.asyncRead(size, function(b64bytes) {
this.sendToPlugin_('onRead', [fd, b64bytes]);
}.bind(this));
};
/**
* Plugin wants to close a file descriptor.
*/
nassh.Nassh.prototype.onPlugin_.close = function(fd) {
var stream = this.streamManager_.getStreamByFd(fd);
if (!stream) {
console.warn('Attempt to close unknown fd: ' + fd);
return;
}
stream.close();
};