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://storage.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