naclterm.js revision f2477e01787aa58f445919b809d89e252beef54f
1/*
2 * Copyright (c) 2013 The Chromium Authors. All rights reserved.
3 * Use of this source code is governed by a BSD-style license that can be
4 * found in the LICENSE file.
5 */
6
7'use strict';
8
9lib.rtdep('lib.f',
10          'hterm');
11
12// CSP means that we can't kick off the initialization from the html file,
13// so we do it like this instead.
14window.onload = function() {
15  lib.init(function() {
16    NaClTerm.init();
17  });
18};
19
20/**
21 * The hterm-powered terminal command.
22 *
23 * This class defines a command that can be run in an hterm.Terminal instance.
24 *
25 * @param {Object} argv The argument object passed in from the Terminal.
26 */
27function NaClTerm(argv) {
28  this.io = argv.io.push();
29  this.argv_ = argv;
30};
31
32var ansiCyan = '\x1b[36m';
33var ansiReset = '\x1b[0m';
34
35/**
36 * Static initialier called from index.html.
37 *
38 * This constructs a new Terminal instance and instructs it to run the NaClTerm
39 * command.
40 */
41NaClTerm.init = function() {
42  var profileName = lib.f.parseQuery(document.location.search)['profile'];
43  var terminal = new hterm.Terminal(profileName);
44  terminal.decorate(document.querySelector('#terminal'));
45
46  // Useful for console debugging.
47  window.term_ = terminal;
48
49  terminal.runCommandClass(NaClTerm, document.location.hash.substr(1));
50  return true;
51};
52
53NaClTerm.prototype.updateStatus = function(message) {
54  document.getElementById('statusField').textContent = message;
55  this.io.print(message + '\n');
56}
57
58/**
59 * Handle messages sent to us from NaCl.
60 *
61 * @private
62 */
63NaClTerm.prototype.handleMessage_ = function(e) {
64  if (e.data.indexOf(NaClTerm.prefix) == 0) {
65    var msg = e.data.substring(NaClTerm.prefix.length);
66    if (!this.loaded) {
67      this.bufferedOutput += msg;
68    } else {
69      this.io.print(msg);
70    }
71  } else if (e.data.indexOf('exited') == 0) {
72    var exitCode = e.data.split(':', 2)[1]
73    if (exitCode === undefined)
74      exitCode = 0;
75    this.exit(exitCode);
76  } else {
77    console.log('unexpected message: ' + e.data);
78    return;
79  }
80}
81
82/**
83 * Handle load error event from NaCl.
84 */
85NaClTerm.prototype.handleLoadAbort_ = function(e) {
86  this.updateStatus('Load aborted.');
87}
88
89/**
90 * Handle load abort event from NaCl.
91 */
92NaClTerm.prototype.handleLoadError_ = function(e) {
93  this.updateStatus(embed.lastError);
94}
95
96NaClTerm.prototype.doneLoadingUrl = function() {
97  var width = this.io.terminal_.screenSize.width;
98  this.io.print('\r' + Array(width+1).join(' '));
99  var message = '\rLoaded ' + this.lastUrl;
100  if (this.lastTotal) {
101    var kbsize = Math.round(this.lastTotal/1024)
102    message += ' ['+ kbsize + ' KiB]';
103  }
104  this.io.print(message.slice(0, width) + '\n')
105}
106
107/**
108 * Handle load end event from NaCl.
109 */
110NaClTerm.prototype.handleLoad_ = function(e) {
111  if (this.lastUrl)
112    this.doneLoadingUrl();
113  else
114    this.io.print('Loaded.\n');
115  delete this.lastUrl
116
117  document.getElementById('loading-cover').style.display = 'none';
118
119  this.io.print(ansiReset);
120
121  // Now that have completed loading and displaying
122  // loading messages we output any messages from the
123  // NaCl module that were buffered up unto this point
124  this.loaded = true;
125  this.io.print(this.bufferedOutput);
126  this.sendMessage(this.bufferedInput);
127  this.bufferedOutput = ''
128  this.bufferedInput = ''
129}
130
131/**
132 * Handle load progress event from NaCl.
133 */
134NaClTerm.prototype.handleProgress_ = function(e) {
135  var url = e.url.substring(e.url.lastIndexOf('/') + 1);
136
137  if (this.lastUrl && this.lastUrl != url)
138    this.doneLoadingUrl()
139
140  if (!url)
141    return;
142
143  var percent = 10;
144  var message = 'Loading ' + url;
145
146  if (e.lengthComputable && e.total > 0) {
147    percent = Math.round(e.loaded * 100 / e.total);
148    var kbloaded = Math.round(e.loaded / 1024);
149    var kbtotal = Math.round(e.total / 1024);
150    message += ' [' + kbloaded + ' KiB/' + kbtotal + ' KiB ' + percent + '%]';
151  }
152
153  document.getElementById('progress-bar').style.width = percent + "%";
154
155  var width = this.io.terminal_.screenSize.width;
156  this.io.print('\r' + message.slice(-width));
157  this.lastUrl = url;
158  this.lastTotal = e.total;
159}
160
161/**
162 * Handle crash event from NaCl.
163 */
164NaClTerm.prototype.handleCrash_ = function(e) {
165 this.exit(this.embed.exitStatus);
166}
167
168/**
169 * Exit the command.
170 */
171NaClTerm.prototype.exit = function(code) {
172 this.io.print(ansiCyan)
173 if (code == -1) {
174   this.io.print('Program crashed (exit status -1)\n')
175 } else {
176   this.io.print('Program exited (status=' + code + ')\n');
177 }
178 this.loaded = false;
179};
180
181NaClTerm.prototype.restartNaCl = function() {
182  if (this.embed !== undefined) {
183    document.getElementById("listener").removeChild(this.embed);
184    delete this.embed;
185  }
186  this.io.terminal_.reset();
187  this.startCommand();
188  this.createEmbed(this.io.terminal_.screenSize.width, this.io.terminal_.screenSize.height);
189}
190
191/**
192 * Create the NaCl embed element.
193 * We delay this until the first terminal resize event so that we start
194 * with the correct size.
195 */
196NaClTerm.prototype.createEmbed = function(width, height) {
197  var mimetype = 'application/x-pnacl';
198  if (navigator.mimeTypes[mimetype] === undefined) {
199    if (mimetype.indexOf('pnacl') != -1)
200      this.updateStatus('Browser does not support PNaCl or PNaCl is disabled');
201    else
202      this.updateStatus('Browser does not support NaCl or NaCl is disabled');
203    return;
204  }
205
206  var embed = document.createElement('object');
207  embed.width = 0;
208  embed.height = 0;
209  embed.data = NaClTerm.nmf;
210  embed.type = mimetype;
211  embed.addEventListener('message', this.handleMessage_.bind(this));
212  embed.addEventListener('progress', this.handleProgress_.bind(this));
213  embed.addEventListener('load', this.handleLoad_.bind(this));
214  embed.addEventListener('error', this.handleLoadError_.bind(this));
215  embed.addEventListener('abort', this.handleLoadAbort_.bind(this));
216  embed.addEventListener('crash', this.handleCrash_.bind(this));
217
218  function addParam(name, value) {
219    var param = document.createElement('param');
220    param.name = name;
221    param.value = value;
222    embed.appendChild(param);
223  }
224
225  addParam('PS_TTY_PREFIX', NaClTerm.prefix);
226  addParam('PS_TTY_RESIZE', 'tty_resize');
227  addParam('PS_TTY_COLS', width);
228  addParam('PS_TTY_ROWS', height);
229  addParam('PS_STDIN', '/dev/tty');
230  addParam('PS_STDOUT', '/dev/tty');
231  addParam('PS_STDERR', '/dev/tty');
232  addParam('PS_VERBOSITY', '2');
233  addParam('PS_EXIT_MESSAGE', 'exited');
234  addParam('TERM', 'xterm-256color');
235  addParam('LUA_DATA_URL', 'http://commondatastorage.googleapis.com/gonacl/demos/publish/234230_dev/lua');
236
237  // Add ARGV arguments from query parameters.
238  var args = lib.f.parseQuery(document.location.search);
239  for (var argname in args) {
240    addParam(argname, args[argname]);
241  }
242
243  // If the application has set NaClTerm.argv and there were
244  // no arguments set in the query parameters then add the default
245  // NaClTerm.argv arguments.
246  if (args['arg1'] === undefined && NaClTerm.argv) {
247    var argn = 1
248    NaClTerm.argv.forEach(function(arg) {
249      var argname = 'arg' + argn;
250      addParam(argname, arg);
251      argn = argn + 1
252    })
253  }
254
255  this.updateStatus('Loading...');
256  this.io.print('Loading NaCl module.\n')
257  document.getElementById("listener").appendChild(embed);
258  this.embed = embed;
259}
260
261NaClTerm.prototype.onTerminalResize_ = function(width, height) {
262  if (this.embed === undefined)
263    this.createEmbed(width, height);
264  else
265    this.embed.postMessage({'tty_resize': [ width, height ]});
266
267  // Require at least 80 columns, otherwise some of the demos look
268  // very wrong.
269  var width = this.io.terminal_.scrollPort_.characterSize.width * 80;
270  document.getElementById("terminal").style.minWidth = width + 'px';
271}
272
273NaClTerm.prototype.sendMessage = function(msg) {
274  if (!this.loaded) {
275    this.bufferedInput += msg;
276    return;
277  }
278  var message = {};
279  message[NaClTerm.prefix] = msg;
280  this.embed.postMessage(message);
281}
282
283NaClTerm.prototype.onVTKeystroke_ = function(str) {
284  this.sendMessage(str)
285}
286
287NaClTerm.prototype.startCommand = function() {
288  // We don't properly support the hterm bell sound, so we need to disable it.
289  this.io.terminal_.prefs_.definePreference('audible-bell-sound', '');
290  this.io.terminal_.setAutoCarriageReturn(true);
291  this.io.terminal_.setCursorPosition(0, 0);
292  this.io.terminal_.setCursorVisible(true);
293
294  this.bufferedOutput = '';
295  this.bufferedInput = '';
296  this.loaded = false;
297  this.io.print(ansiCyan);
298}
299
300/*
301 * This is invoked by the terminal as a result of terminal.runCommandClass().
302 */
303NaClTerm.prototype.run = function() {
304  this.startCommand();
305  this.io.onVTKeystroke = this.onVTKeystroke_.bind(this);
306  this.io.onTerminalResize = this.onTerminalResize_.bind(this);
307};
308