scp implementation for axiom

Change-Id: I35f6794aff32577f2981bba7b1b4843922050187
Reviewed-on: https://chromium-review.googlesource.com/260583
Reviewed-by: Rob Ginda <rginda@chromium.org>
Tested-by: Rob Ginda <rginda@chromium.org>
diff --git a/nassh/js/nassh_command_instance.js b/nassh/js/nassh_command_instance.js
index 3f1bbb0..a5a261f 100644
--- a/nassh/js/nassh_command_instance.js
+++ b/nassh/js/nassh_command_instance.js
@@ -663,7 +663,6 @@
  */
 nassh.CommandInstance.prototype.sendToPlugin_ = function(name, args) {
   var str = JSON.stringify({name: name, arguments: args});
-
   this.plugin_.postMessage(str);
 };
 
@@ -849,7 +848,6 @@
   };
 
   stream.onClose = function(reason) {
-    console.log('close: ' + fd);
     self.sendToPlugin_('onClose', [fd, reason]);
   };
 };
@@ -891,22 +889,6 @@
 };
 
 /**
- * Notify the plugin that data is available to read.
- */
-nassh.CommandInstance.prototype.onPlugin_.isReadReady = function(fd) {
-  var self = this;
-  var stream = self.streamManager_.getStreamByFd(fd);
-
-  if (!stream) {
-    console.warn('Attempt to call isReadReady from unknown fd: ' + fd);
-    return;
-  }
-
-  var rv = stream.isReadReady();
-  self.sendToPlugin_('onIsReadReady', [fd, rv]);
-};
-
-/**
  * Plugin wants to close a file descriptor.
  */
 nassh.CommandInstance.prototype.onPlugin_.close = function(fd) {
diff --git a/nassh/js/nassh_exe_scp.js b/nassh/js/nassh_exe_scp.js
new file mode 100644
index 0000000..5719383
--- /dev/null
+++ b/nassh/js/nassh_exe_scp.js
@@ -0,0 +1,663 @@
+// Copyright (c) 2015 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.
+
+nassh.exe.scp = function(cx) {
+  var scp = new nassh.Scp(cx);
+  scp.run().then(function(value) {
+    cx.closeOk(0);
+  }, function(err) {
+    cx.closeError(err);
+  });
+
+  return cx.ephemeralPromise;
+};
+
+nassh.exe.scp.signature = {
+  'help|h': '?',
+  'r': '?',
+  'p': '?',
+  'v': '?',
+  "_": '@'
+};
+
+nassh.Scp = function(cx) {
+  this.resolveRead_ = null;
+  this.readBuffer_ = '';
+  this.errors_ = 0;
+  this.remoteClosed_ = false;
+
+  this.mtimeSec_ = 0;
+  this.mtimeUsec_ = 0;
+  this.atimeSec_ = 0;
+  this.atimeUsec_ = 0;
+
+  this.fileMode_ = 0;
+  this.fileSize_ = 0;
+  this.fileName_ = '';
+
+  this.destIsDir_ = false;
+  this.destFile_ = '';
+  this.dirStack_ = [];
+
+  this.verbose_ = false;
+  this.isRecursive_ = false;
+  this.withTimestamp_ = false;
+
+  this.cx = cx;
+};
+
+nassh.Scp.prototype.run = function() {
+  var CMD_USAGE_STRING =
+      'usage: scp [[user]@host1:]file1 ... [[user@]host2:]file2';
+  this.cx.ready();
+  var list = this.cx.getArg('_', []);
+  if (list.length < 2 || this.cx.getArg('help')) {
+    this.cx.stderr.write(CMD_USAGE_STRING + '\n');
+    return this.cx.closeOk();
+  }
+
+  // TODO(binji): remove this code until HERE. Just a hack to support flags.
+  var nonFlags = [];
+  var i;
+  var j;
+  for (i = 0; i < list.length; ++i) {
+    var item = list[i];
+    if (item.lastIndexOf('-', 0) == 0) {
+      if (item[1] != '-') {
+        for (j = 1; j < item.length; ++j) {
+          if (item[j] == 'r') {
+            this.isRecursive_ = true;
+          }
+          if (item[j] == 'p') {
+            this.withTimestamp_ = true;
+          }
+          if (item[j] == 'v') {
+            this.verbose_ = true;
+          }
+        }
+      } else {
+        // Long flag name.
+      }
+    } else {
+      nonFlags.push(item);
+    }
+  }
+  list = nonFlags;
+  // HERE
+
+  if (this.cx.getArg('r')) {
+    this.isRecursive_ = true;
+  }
+
+  if (this.cx.getArg('p')) {
+    this.withTimestamp_ = true;
+  }
+
+  if (this.cx.getArg('v')) {
+    this.verbose_ = true;
+  }
+
+  var sources = list.slice(0, list.length - 1);
+  var dest = list[list.length - 1];
+  var destHostFile = this.parseHostFile_(dest);
+
+  this.destFile_ = destHostFile.file;
+
+  if (destHostFile.host != '') {
+    return this.toRemote_(destHostFile, sources);
+  } else {
+    return this.toLocal_(sources);
+  }
+};
+
+nassh.Scp.prototype.parseHostFile_ = function(arg) {
+  var flag = 0;
+  var i;
+  var user = '';
+  var host = '';
+  var file = arg;
+
+  if (arg[i] == ':') {  // leading colon is part of file name.
+    return {host: host, user: user, file: file};
+  }
+
+  if (arg[i] == '[') {
+    flag = 1;
+  }
+
+  for (i = 0; i < arg.length; ++i) {
+    if (arg[i] == '@' && arg[i + 1] == '[') {
+      flag = 1;
+    }
+
+    if (arg[i] == ']' && arg[i + 1] == ':' && flag) {
+      host = arg.slice(0, i + 1);
+      file = arg.slice(i + 2);
+      break;
+    }
+
+    if (arg[i] == ':' && !flag) {
+      host = arg.slice(0, i);
+      file = arg.slice(i + 1);
+      break;
+    }
+
+    if (arg[i] == '/') {
+      return {host: host, user: user, file: file};
+    }
+  }
+
+  if (host == '') {
+    return {host: host, user: user, file: file};
+  }
+
+  // Check whether "host" is a fileSystem name. If so, assume that the user
+  // actually wants to write to a local fileSystem, and not a server with that
+  // name. It is always valid to wrap the hostname in (e.g. "[hostname]") if
+  // the other behavior is desired.
+  var fsm = this.cx.fileSystemManager;
+  var fss = fsm.getFileSystems();
+  for (i = 0; i < fss.length; ++i) {
+    if (fss[i].name == host) {
+      return {host: '', user: '', file: arg};
+    }
+  }
+
+  var at = host.lastIndexOf('@');
+
+  if (at != -1) {
+    user = host.slice(0, at);
+    host = host.slice(at + 1);
+  }
+
+  // Clean the host (i.e. remove square brackets, if any).
+  if (host[0] == '[' && host[host.length - 1] == ']') {
+    host = host.slice(1, host.length - 1);
+  }
+
+  return {host: host, user: user, file: file};
+}
+
+nassh.Scp.prototype.toRemote_ = function(destHostFile, sources) {
+  var p = this.doCmd_(this.makeCommand_(destHostFile, false))
+              .then(this.readResponseAndHandleError_.bind(this));
+
+  sources.forEach(function(source) {
+    var sourceHostFile = this.parseHostFile_(source);
+    if (sourceHostFile.host != '') {
+      // Remote to remote.
+      // TODO(binji): implement?
+      this.cx.stderr.write('Failed to copy ' + source +
+                           '. Remote to remote not supported.\n');
+      return;
+    } else {
+      // Local to remote.
+      var sourcePath = new axiom.fs.path.Path(sourceHostFile.file);
+      p = p.then(function() {
+            return this.getPathInfo_(sourcePath);
+          }.bind(this))
+          .then(function(result) {
+            if (!result.exists) {
+              return this.errorExit('File not found: ' + sourceHostFile.file);
+            }
+
+            if (result.isDir && !this.isRecursive_) {
+              // TODO(binji): support
+              return this.errorExit(sourceHostFile.file +
+                                    ': not a regular file');
+            }
+
+            if (result.isDir) {
+              return this.sendRemoteDirectory_(sourcePath, result.stat);
+            } else {
+              return this.sendRemoteFile_(sourcePath, result.stat);
+            }
+          }.bind(this));
+    }
+  }.bind(this));
+
+  return p;
+};
+
+nassh.Scp.prototype.makeCommand_ = function(hostFile, remoteIsSource) {
+  var command = [
+    '-x',
+    '-oForwardAgent=no',
+    '-oPermitLocalCommand=no',
+    '-oClearAllForwardings=yes',
+  ];
+
+  if (hostFile.user != '') {
+    command.push('-l');
+    command.push(hostFile.user);
+  }
+
+  command.push('--');
+  command.push(hostFile.host);
+  command.push('scp');
+  command.push('-q');
+  if (remoteIsSource) {
+    command.push('-f');  // from
+  } else {
+    command.push('-t');  // to
+  }
+
+  if (this.verbose_) {
+    command.push('-v');
+  }
+  if (this.isRecursive_) {
+    command.push('-r');
+  }
+  if (this.withTimestamp_) {
+    command.push('-p');
+  }
+  command.push(hostFile.file);
+  return command;
+};
+
+nassh.Scp.prototype.getPathInfo_ = function(path) {
+  return this.cx.fileSystemManager.stat(path).then(
+      // stat success
+      function(statResult) {
+        return {
+          exists: true,
+          isDir: (statResult.mode & axiom.fs.path.Path.Mode.D) != 0,
+          stat: statResult
+        };
+      },
+      // stat fail
+      function(error) {
+        return {exists: false, isDir: false, stat: null};
+      });
+};
+
+nassh.Scp.prototype.sendRemoteDirectory_ = function(sourcePath, sourceStat) {
+  var p;
+
+  if (this.withTimestamp_) {
+    // T<mtime> 0 <atime> 0\n
+    // TODO(binji): The second value should be atime, but that isn't supported
+    // in axiom (yet?)
+    var timestamp = 'T' + sourceStat.mtime + ' 0 ' + sourceStat.mtime + ' 0\n';
+    this.remoteWrite_(timestamp);
+
+    p = this.readResponseAndHandleError_();
+  } else {
+    p = Promise.resolve();
+  }
+
+  return p.then(function() {
+        // D<mode in octal> 0 <file name>\n
+        // TODO(binji): read file mode when/if supported by axiom.
+        var control = 'D0755 0 ' + sourcePath.getBaseName() + '\n';
+        this.remoteWrite_(control);
+        return this.readResponseAndHandleError_();
+      }.bind(this))
+      .then(function() {
+        return this.cx.fileSystemManager.list(sourcePath);
+      }.bind(this))
+      .then(function(listResult) {
+        var names = Object.keys(listResult).sort();
+        var p = Promise.resolve();
+
+        names.forEach(function(name) {
+          var stat = listResult[name];
+          var path = sourcePath.combine(name);
+
+          if ((stat.mode & axiom.fs.path.Path.Mode.D) != 0) {
+            p = p.then(function() {
+              return this.sendRemoteDirectory_(path, stat);
+            }.bind(this));
+          } else {
+            p = p.then(function() {
+              return this.sendRemoteFile_(path, stat);
+            }.bind(this));
+          }
+        }.bind(this));
+
+        p = p.then(function() {
+          this.remoteWrite_('E\n');
+          return this.readResponseAndHandleError_();
+        }.bind(this));
+
+        return p;
+      }.bind(this));
+};
+
+nassh.Scp.prototype.remoteWrite_ = function(value) {
+  this.stdioSource.stdin.write(value);
+};
+
+nassh.Scp.prototype.readResponseAndHandleError_ = function() {
+  return this.readBytes_(1).then(this.handleError_.bind(this));
+};
+
+nassh.Scp.prototype.readBytes_ = function(numBytes) {
+  var onRead = function(success) {
+    if (!success) {
+      return Promise.resolve(null);
+    }
+
+    if (this.readBuffer_.length < numBytes) {
+      return this.read_().then(onRead);
+    }
+
+    var data = this.readBuffer_.slice(0, numBytes);
+    this.readBuffer_ = this.readBuffer_.slice(numBytes);
+    return Promise.resolve(data);
+  }.bind(this);
+
+  // Try reading from the buffer first.
+  return onRead(true);
+};
+
+nassh.Scp.prototype.read_ = function() {
+  if (this.remoteClosed_) {
+    if (this.readBuffer_.length > 0) {
+      console.error('Buffer has extra unparsed data. length ' +
+                    this.readBuffer_.length + '\n' + this.readBuffer_);
+    }
+
+    return Promise.resolve(false);
+  }
+
+  if (this.resolveRead_ != null) {
+    // TODO(binji): better assert?
+    throw new Error('resolveRead_ not null when calling read_.');
+  }
+
+  return new Promise(function(resolve, reject) {
+    this.resolveRead_ = function(value) {
+      this.resolveRead_ = null;
+      resolve(value);
+    }.bind(this);
+  }.bind(this));
+};
+
+nassh.Scp.prototype.sendRemoteFile_ = function(sourcePath, sourceStat) {
+  var dataType = axiom.fs.data_type.DataType.UTF8String;
+  var openMode = axiom.fs.open_mode.OpenMode.fromString('r');
+  var readResult;
+  var p;
+
+  if (this.withTimestamp_) {
+    // T<mtime> 0 <atime> 0\n
+    // TODO(binji): The second value should be atime, but that isn't supported
+    // in axiom (yet?)
+    var timestamp = 'T' + sourceStat.mtime + ' 0 ' + sourceStat.mtime + ' 0\n';
+    this.remoteWrite_(timestamp);
+
+    p = this.readResponseAndHandleError_();
+  } else {
+    p = Promise.resolve();
+  }
+
+  return p.then(function() {
+        return this.cx.fileSystemManager.readFile(sourcePath, dataType,
+                                                  openMode);
+      }.bind(this))
+      .then(function(result) {
+        readResult = result;
+        var control = 'C0644 ' + readResult.data.length + ' ' +
+                      sourcePath.getBaseName() + '\n';
+        this.remoteWrite_(control);
+        return this.readResponseAndHandleError_();
+      }.bind(this))
+      .then(function() {
+        // TODO(binji): Write the file data to the plugin in smaller chunks.
+        // TODO(binji): Should print an error here (instead of \0) if the write
+        // failed.
+        this.remoteWrite_(readResult.data + '\0');
+        return this.readResponseAndHandleError_();
+      }.bind(this));
+};
+
+nassh.Scp.prototype.handleError_ = function(errorCode) {
+  if (errorCode == '\0') {
+    return;
+  }
+
+  return this.readLine_()
+      .then(function(line) {
+        line += '\n';
+        if (errorCode == '\u0002') {
+          this.cx.stderr.write(line);
+          return Promise.reject(line);
+        } else {
+          this.cx.stderr.write(line);
+        }
+      }.bind(this));
+};
+
+nassh.Scp.prototype.toLocal_ = function(sources) {
+  var destPath = new axiom.fs.path.Path(this.destFile_);
+  var p;
+
+  p = this.getPathInfo_(destPath)
+      .then(function(result) {
+        this.destIsDir_ = result.isDir;
+        if (sources.length > 1 && !this.destIsDir_) {
+          return Promise.reject('Expected ' + this.destFile_ +
+                                ' to be directory.');
+        }
+      }.bind(this));
+
+  sources.forEach(function(source) {
+    var sourceHostFile = this.parseHostFile_(source);
+    if (sourceHostFile.host == '') {
+      // Local to local.
+      // TODO(binji): implement via cp
+      this.cx.stderr.write('Failed to copy ' + sourceHostFile.file +
+                           '. Local to local not supported.\n');
+      return;
+    } else {
+      // Remote to local.
+      p = p.then(function() {
+            return this.doCmd_(this.makeCommand_(sourceHostFile, true));
+          }.bind(this))
+          .then(function() {
+            this.remoteWrite_('\0');
+          }.bind(this))
+          .then(this.readAndParseLine_.bind(this));
+    }
+  }.bind(this));
+
+  return p;
+};
+
+nassh.Scp.prototype.readAndParseLine_ = function() {
+  return this.readLine_().then(this.parseLine_.bind(this));
+};
+
+nassh.Scp.prototype.doCmd_ = function(args) {
+  var nassh = new axiom.fs.path.Path('jsfs:/exe/nassh');
+  this.stdioSource = new axiom.fs.stdio_source.StdioSource();
+
+  return this.cx.fileSystemManager.createExecuteContext(
+                                       nassh, this.stdioSource.stdio, args)
+      .then(function(cx) {
+        this.sshcx = cx;
+        cx.dependsOn(this.cx);
+
+        cx.onClose.addListener(function(reason, value) {
+          this.remoteClosed_ = true;
+          if (this.resolveRead_) {
+            this.resolveRead_(false);
+          }
+        }.bind(this));
+
+        this.stdioSource.stdout.onData.addListener(function(value) {
+          this.readBuffer_ += value;
+          if (this.resolveRead_) {
+            this.resolveRead_(true);
+          }
+        }.bind(this));
+        this.stdioSource.stdout.resume();
+
+        this.stdioSource.stderr.onData.addListener(
+            this.cx.stderr.write.bind(this.cx.stderr));
+        this.stdioSource.stderr.resume();
+
+        this.stdioSource.ttyout.onData.addListener(
+            this.cx.stderr.write.bind(this.cx.stderr));
+        this.stdioSource.ttyout.resume();
+
+        this.cx.stdin.onData.addListener(
+            this.stdioSource.ttyin.write.bind(this.stdioSource.ttyin));
+
+        cx.execute();
+      }.bind(this));
+};
+
+nassh.Scp.prototype.readLine_ = function() {
+  var onRead = function(success) {
+    if (!success) {
+      return Promise.resolve(null);
+    }
+
+    var nl = this.readBuffer_.indexOf('\n');
+    if (nl == -1) {
+      return this.read_().then(onRead);
+    }
+
+    var line = this.readBuffer_.slice(0, nl);
+    this.readBuffer_ = this.readBuffer_.slice(nl + 1);
+    return Promise.resolve(line);
+  }.bind(this);
+
+  // Try reading from the buffer first.
+  return onRead(true);
+};
+
+nassh.Scp.prototype.parseLine_ = function(line) {
+  if (line == null) {
+    // No more data, the remote end must have closed.
+    return Promise.resolve();
+  }
+
+  var c = line[0];
+  var msg;
+  var match;
+
+  switch (c) {
+    case '\u0001':
+    case '\u0002':
+      // Error.
+      this.errors_++;
+      msg = line.slice(1);
+      this.cx.stderr.write(msg + '\n');
+
+      if (c == '\u0002') {
+        // Fatal error.
+        return Promise.reject(msg);
+      }
+      break;
+
+    case 'T':
+      // Timestamp
+      match = /T(\d+) (\d+) (\d+) (\d+)/.exec(line);
+      if (!match) {
+        return this.errorExit('unable to parse timestamp: ' + line);
+      }
+
+      this.mtimeSec_ = parseInt(match[1], 10);
+      this.mtimeUsec_ = parseInt(match[2], 10);
+      this.atimeSec_ = parseInt(match[3], 10);
+      this.atimeUsec_ = parseInt(match[4], 10);
+      this.remoteWrite_('\0');
+      break;
+
+    case 'C':
+    case 'D':
+      match = /(C|D)([0-7]+) (\d+) (.*)/.exec(line);
+      if (!match) {
+        return this.errorExit('unable to parse control line: ' + line);
+      }
+
+      this.fileMode_ = parseInt(match[2], 8);
+      this.fileSize_ = parseInt(match[3], 10);
+      this.fileName_ = match[4];
+      this.remoteWrite_('\0');
+
+      if (c == 'C') {
+        // Regular file
+        // TODO(binji): read/write file at the same time, rather than reading
+        // it all in at once.
+        return this.readBytes_(this.fileSize_)
+            .then(this.writeLocalFile_.bind(this))
+            .then(this.readResponseAndHandleError_.bind(this))
+            .then(function() {
+              this.remoteWrite_('\0');
+            }.bind(this))
+            .then(this.readAndParseLine_.bind(this));
+      } else {
+        // Directory
+        var path = this.makePath_();
+        return this.getPathInfo_(path)
+            .then(function(result) {
+              if (result.exists) {
+                // File exists, make sure it is a directory.
+                if (!result.isDir) {
+                  return this.errorExit('expected \"' + path +
+                                        '\" to be directory');
+                }
+              } else {
+                // Directory doesn't exist? Try to create it.
+                return this.cx.fileSystemManager.mkdir(path);
+              }
+            }.bind(this))
+            .then(function() {
+              this.dirStack_.push(this.fileName_);
+            }.bind(this))
+            .then(this.readAndParseLine_.bind(this));
+      }
+      break;
+
+    case 'E':
+      this.dirStack_.pop();
+      this.remoteWrite_('\0');
+      break;
+
+    default:
+      return this.errorExit('expected control record');
+  }
+
+  return this.readAndParseLine_();
+};
+
+nassh.Scp.prototype.makePath_ = function() {
+  var path = new axiom.fs.path.Path(this.destFile_);
+  if (this.destIsDir_) {
+    if (this.dirStack_) {
+      path = path.combine(this.dirStack_.join('/'));
+    }
+
+    path = path.combine(this.fileName_);
+  }
+
+  return path;
+};
+
+nassh.Scp.prototype.writeLocalFile_ = function(data) {
+  var path = this.makePath_();
+  var dataType = axiom.fs.data_type.DataType.UTF8String;
+  var openMode = axiom.fs.open_mode.OpenMode.fromString('ctw');
+
+  // TODO(binji): Set timestamp and file mode...?
+  return this.cx.fileSystemManager.writeFile(path, dataType, data, openMode);
+};
+
+nassh.Scp.prototype.error = function(msg) {
+  msg += '\n';
+  this.remoteWrite_('\u0001' + msg);
+  this.cx.stderr.write(msg);
+};
+
+nassh.Scp.prototype.errorExit = function(msg) {
+  msg += '\n';
+  this.remoteWrite_('\u0002' + msg);
+  this.cx.stderr.write(msg);
+  // TODO(binji): Wait until msg is sent to remote end? How to do this?
+  return Promise.reject(msg);
+};
diff --git a/nassh/js/nassh_nassh.js b/nassh/js/nassh_nassh.js
index a742f4a..d8b4f6a 100644
--- a/nassh/js/nassh_nassh.js
+++ b/nassh/js/nassh_nassh.js
@@ -10,18 +10,23 @@
  * 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.onStdIn.addListener(this.onStdIn_, this);
+  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.arg;
+  var ecArg = executeContext.args.arg;
 
   if (ecArg instanceof Array) {
     ecArg = {argv: ecArg};
@@ -61,6 +66,14 @@
 };
 
 /**
+ * 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.
@@ -97,11 +110,11 @@
 };
 
 nassh.Nassh.prototype.print = function(str, opt_onAck) {
-  this.executeContext.stdout(str, opt_onAck);
+  this.executeContext.stderr.write(str, opt_onAck);
 };
 
 nassh.Nassh.prototype.println = function(str, opt_onAck) {
-  this.executeContext.stdout(str + '\n');
+  this.executeContext.stderr.write(str + '\n');
 };
 
 nassh.Nassh.prototype.initPlugin_ = function(onComplete) {
@@ -112,27 +125,29 @@
   this.plugin_.setAttribute('type', 'application/x-nacl');
 
   this.plugin_.addEventListener('load', function() {
-      this.println(nassh.msg('PLUGIN_LOADING_COMPLETE'));
-      setTimeout(this.onTTYChange_.bind(this));
-      onComplete();
-    }.bind(this));
+    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;
+    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));
+    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));
+  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);
 
@@ -170,6 +185,16 @@
   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;
@@ -211,30 +236,41 @@
  * @return {Object} The newly created stream.
  */
 nassh.Nassh.prototype.createTtyStream = function(
-    fd, allowRead, allowWrite, onOpen) {
-  var self = this;
+    fd, path, allowRead, allowWrite, onOpen) {
+  var cx = this.executeContext;
   var arg = {
     fd: fd,
     allowRead: allowRead,
     allowWrite: allowWrite,
-    inputBuffer: self.inputBuffer_,
-    onWrite: self.executeContext.stdout.bind(self.executeContext)
   };
 
+  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 = self.streamManager_.openStream(streamClass, fd, arg, onOpen);
+  var stream = this.streamManager_.openStream(streamClass, fd, arg, onOpen);
   if (allowRead) {
     var onDataAvailable = function(isAvailable) {
       // Send current read status to plugin.
-      self.sendToPlugin_('onReadReady', [fd, isAvailable]);
-    };
+      setTimeout(function() {
+        this.sendToPlugin_('onReadReady', [fd, isAvailable]);
+      }.bind(this), 0);
+    }.bind(this);
 
-    self.inputBuffer_.onDataAvailable.addListener(onDataAvailable);
+    arg.inputBuffer.onDataAvailable.addListener(onDataAvailable);
 
     stream.onClose = function(reason) {
-      self.inputBuffer_.onDataAvailable.removeListener(onDataAvailable);
-      self.sendToPlugin_('onClose', [fd, reason]);
-    };
+      arg.inputBuffer.onDataAvailable.removeListener(onDataAvailable);
+      this.sendToPlugin_('onClose', [fd, reason]);
+    }.bind(this);
   }
 
   return stream;
@@ -247,36 +283,32 @@
  * /dev/tty.
  */
 nassh.Nassh.prototype.onPlugin_.openFile = function(fd, path, mode) {
-  var self = this;
-  var isAtty = false;
+  // 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);
 
-  var DEV_TTY = '/dev/tty';
-  var DEV_STDIN = '/dev/stdin';
-  var DEV_STDOUT = '/dev/stdout';
-  var DEV_STDERR = '/dev/stderr';
-
-  if (path == DEV_TTY || path == DEV_STDIN || path == DEV_STDOUT ||
-      path == DEV_STDERR) {
-    var allowRead = path == DEV_STDIN || path == DEV_TTY;
-    var allowWrite = path == DEV_STDOUT || path == DEV_STDERR || path == DEV_TTY;
-    var stream = self.createTtyStream(fd, allowRead, allowWrite, function(success) {
-      self.sendToPlugin_('onOpenFile', [fd, success, isAtty]);
-    });
-    isAtty = true;
+  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 {
-    self.sendToPlugin_('onOpenFile', [fd, false, false]);
+    this.sendToPlugin_('onOpenFile', [fd, false, false]);
   }
 };
 
 /**
  * Plugin wants to write some data to a file descriptor.
- *
- * This is only used for stdout/stderr.  It used to be used as a conduit to
- * the HTML5 filesystem, but now NaCl can get there directly.
  */
 nassh.Nassh.prototype.onPlugin_.write = function(fd, data) {
-  var self = this;
-  var stream = self.streamManager_.getStreamByFd(fd);
+  var stream = this.streamManager_.getStreamByFd(fd);
 
   if (!stream) {
     console.warn('Attempt to write to unknown fd: ' + fd);
@@ -284,16 +316,15 @@
   }
 
   stream.asyncWrite(data, function(writeCount) {
-      self.sendToPlugin_('onWriteAcknowledge', [fd, writeCount]);
-    }, 100);
+      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 self = this;
-  var stream = self.streamManager_.getStreamByFd(fd);
+  var stream = this.streamManager_.getStreamByFd(fd);
 
   if (!stream) {
     console.warn('Attempt to read from unknown fd: ' + fd);
@@ -301,16 +332,15 @@
   }
 
   stream.asyncRead(size, function(b64bytes) {
-      self.sendToPlugin_('onRead', [fd, b64bytes]);
-    });
+      this.sendToPlugin_('onRead', [fd, b64bytes]);
+    }.bind(this));
 };
 
 /**
  * Plugin wants to close a file descriptor.
  */
 nassh.Nassh.prototype.onPlugin_.close = function(fd) {
-  var self = this;
-  var stream = self.streamManager_.getStreamByFd(fd);
+  var stream = this.streamManager_.getStreamByFd(fd);
   if (!stream) {
     console.warn('Attempt to close unknown fd: ' + fd);
     return;
diff --git a/nassh/manifest.json b/nassh/manifest.json
index 000b73f..b8ca5e2 100644
--- a/nassh/manifest.json
+++ b/nassh/manifest.json
@@ -38,6 +38,7 @@
       "js/nassh_exe.js",
       "js/nassh_exe_hterm.js",
       "js/nassh_exe_nassh.js",
+      "js/nassh_exe_scp.js",
       "js/nassh_preference_manager.js",
       "js/nassh_nassh.js",
       "js/nassh_background.js",