| // Copyright (c) 2012 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. |
| |
| goog.provide('cros.factory.Goofy'); |
| |
| goog.require('goog.crypt'); |
| goog.require('goog.crypt.base64'); |
| goog.require('goog.crypt.Sha1'); |
| goog.require('goog.date.Date'); |
| goog.require('goog.date.DateTime'); |
| goog.require('goog.debug.ErrorHandler'); |
| goog.require('goog.debug.FancyWindow'); |
| goog.require('goog.debug.Logger'); |
| goog.require('goog.dom'); |
| goog.require('goog.dom.classes'); |
| goog.require('goog.dom.iframe'); |
| goog.require('goog.events'); |
| goog.require('goog.events.EventHandler'); |
| goog.require('goog.events.KeyCodes'); |
| goog.require('goog.i18n.DateTimeFormat'); |
| goog.require('goog.i18n.NumberFormat'); |
| goog.require('goog.json'); |
| goog.require('goog.math'); |
| goog.require('goog.net.WebSocket'); |
| goog.require('goog.net.XhrIo'); |
| goog.require('goog.string'); |
| goog.require('goog.style'); |
| goog.require('goog.Uri'); |
| goog.require('goog.ui.AdvancedTooltip'); |
| goog.require('goog.ui.Checkbox'); |
| goog.require('goog.ui.Dialog'); |
| goog.require('goog.ui.Dialog.ButtonSet'); |
| goog.require('goog.ui.MenuSeparator'); |
| goog.require('goog.ui.PopupMenu'); |
| goog.require('goog.ui.ProgressBar'); |
| goog.require('goog.ui.Prompt'); |
| goog.require('goog.ui.Select'); |
| goog.require('goog.ui.SplitPane'); |
| goog.require('goog.ui.SubMenu'); |
| goog.require('goog.ui.tree.TreeControl'); |
| goog.require('goog.window'); |
| |
| cros.factory.logger = goog.debug.Logger.getLogger('cros.factory'); |
| |
| /** |
| * @define {boolean} Whether to automatically collapse items once tests have |
| * completed. |
| */ |
| cros.factory.AUTO_COLLAPSE = false; |
| |
| /** |
| * Keep-alive interval for the WebSocket. (Chrome times out |
| * WebSockets every ~1 min, so 30 s seems like a good interval.) |
| * @const |
| * @type number |
| */ |
| cros.factory.KEEP_ALIVE_INTERVAL_MSEC = 30000; |
| |
| /** |
| * Interval at which to update system status. |
| * @const |
| * @type number |
| */ |
| cros.factory.SYSTEM_STATUS_INTERVAL_MSEC = 5000; |
| |
| /** |
| * Interval at which to try mounting the USB drive. |
| * @const |
| * @type number |
| */ |
| cros.factory.MOUNT_USB_DELAY_MSEC = 1000; |
| |
| /** |
| * Width of the control panel, as a fraction of the viewport size. |
| * @type number |
| */ |
| cros.factory.CONTROL_PANEL_WIDTH_FRACTION = 0.2; |
| |
| /** |
| * Minimum width of the control panel, in pixels. |
| * @type number |
| */ |
| cros.factory.CONTROL_PANEL_MIN_WIDTH = 275; |
| |
| /** |
| * Height of the log pane, as a fraction of the viewport size. |
| * @type number |
| */ |
| cros.factory.LOG_PANE_HEIGHT_FRACTION = 0.2; |
| |
| /** |
| * Minimum height of the log pane, in pixels. |
| * @type number |
| */ |
| cros.factory.LOG_PANE_MIN_HEIGHT = 170; |
| |
| /** |
| * Maximum size of a dialog (width or height) as a fraction of viewport size. |
| * @type number |
| */ |
| cros.factory.MAX_DIALOG_SIZE_FRACTION = 0.75; |
| |
| /** |
| * Makes a label that displays English (or optionally Chinese). |
| * @param {string} en |
| * @param {string=} zh |
| */ |
| cros.factory.Label = function(en, zh) { |
| return '<span class="goofy-label-en">' + en + '</span>' + |
| '<span class="goofy-label-zh">' + (zh || en) + '</span>'; |
| }; |
| |
| /** |
| * Makes control content that displays English (or optionally Chinese). |
| * |
| * Note that this actually returns a Node, but we call it an unknown |
| * type so it will be accepted by various buggy methods such as |
| * goog.ui.Dialog.setTitle. |
| * |
| * @param {string} en |
| * @param {string=} zh |
| * @return {?} |
| */ |
| cros.factory.Content = function(en, zh) { |
| var span = document.createElement('span'); |
| span.innerHTML = cros.factory.Label(en, zh); |
| return span; |
| }; |
| |
| /** |
| * Labels for items in system info. |
| * @type Array.<Object.<string, string>> |
| */ |
| cros.factory.SYSTEM_INFO_LABELS = [ |
| {key: 'serial_number', label: cros.factory.Label('Serial Number')}, |
| {key: 'factory_image_version', |
| label: cros.factory.Label('Factory Image Version')}, |
| {key: 'wlan0_mac', label: cros.factory.Label('WLAN MAC')}, |
| {key: 'ips', label: cros.factory.Label('IP Addresses')}, |
| {key: 'kernel_version', label: cros.factory.Label('Kernel')}, |
| {key: 'architecture', label: cros.factory.Label('Architecture')}, |
| {key: 'ec_version', label: cros.factory.Label('EC')}, |
| {key: 'firmware_version', label: cros.factory.Label('Firmware')}, |
| {key: 'root_device', label: cros.factory.Label('Root Device')}, |
| {key: 'factory_md5sum', label: cros.factory.Label('Factory MD5SUM'), |
| transform: function(value) { |
| return value || cros.factory.Label('(no update)'); |
| }} |
| ]; |
| |
| cros.factory.UNKNOWN_LABEL = '<span class="goofy-unknown">' + |
| cros.factory.Label('Unknown') + '</span>'; |
| |
| /** |
| * An item in the test list. |
| * @typedef {{path: string, label_en: string, label_zh: string, |
| * kbd_shortcut: string, subtests: Array, disable_abort: boolean}} |
| */ |
| cros.factory.TestListEntry; |
| |
| /** |
| * A pending shutdown event. |
| * @typedef {{delay_secs: number, time: number, operation: string, |
| * iteration: number, iterations: number }} |
| */ |
| cros.factory.PendingShutdownEvent; |
| |
| /** |
| * Public API for tests. |
| * @constructor |
| * @param {cros.factory.Invocation} invocation |
| */ |
| cros.factory.Test = function(invocation) { |
| /** |
| * @type cros.factory.Invocation |
| */ |
| this.invocation = invocation; |
| |
| /** |
| * Map of char codes to handlers. Null if not yet initialized. |
| * @type {?Object.<number, function()>} |
| */ |
| this.keyHandlers = null; |
| }; |
| |
| /** |
| * Passes the test. |
| * @export |
| */ |
| cros.factory.Test.prototype.pass = function() { |
| this.invocation.goofy.sendEvent( |
| 'goofy:end_test', { |
| 'status': 'PASSED', |
| 'invocation': this.invocation.uuid, |
| 'test': this.invocation.path |
| }); |
| this.invocation.dispose(); |
| }; |
| |
| /** |
| * Fails the test with the given error message. |
| * @export |
| * @param {string} errorMsg |
| */ |
| cros.factory.Test.prototype.fail = function(errorMsg) { |
| this.invocation.goofy.sendEvent('goofy:end_test', { |
| 'status': 'FAILED', |
| 'error_msg': errorMsg, |
| 'invocation': this.invocation.uuid, |
| 'test': this.invocation.path |
| }); |
| this.invocation.dispose(); |
| }; |
| |
| /** |
| * Sends an event to the test backend. |
| * @export |
| * @param {string} subtype the event type |
| * @param {string} data the event data |
| */ |
| cros.factory.Test.prototype.sendTestEvent = function(subtype, data) { |
| this.invocation.goofy.sendEvent('goofy:test_ui_event', { |
| 'test': this.invocation.path, |
| 'invocation': this.invocation.uuid, |
| 'subtype': subtype, |
| 'data': data |
| }); |
| }; |
| |
| /** |
| * Binds a key to a handler. |
| * @param {number} keyCode the key code to bind. |
| * @param {function()} handler the function to call when the key is pressed. |
| * @export |
| */ |
| cros.factory.Test.prototype.bindKey = function(keyCode, handler) { |
| if (!this.keyHandlers) { |
| this.keyHandlers = new Object(); |
| // Set up the listener. |
| goog.events.listen( |
| this.invocation.iframe.contentWindow, |
| goog.events.EventType.KEYUP, |
| function(event) { |
| handler = this.keyHandlers[event.keyCode]; |
| if (handler) { |
| handler(); |
| } |
| }, false, this); |
| } |
| this.keyHandlers[keyCode] = handler; |
| }; |
| |
| /** |
| * Unbinds a key and removes its handler. |
| * @param {number} keyCode the key code to unbind. |
| * @export |
| */ |
| cros.factory.Test.prototype.unbindKey = function(keyCode) { |
| if (this.keyHandlers && keyCode in this.keyHandlers) { |
| delete this.keyHandlers[keyCode]; |
| } |
| } |
| |
| /** |
| * Triggers an update check. |
| */ |
| cros.factory.Test.prototype.updateFactory = function() { |
| this.invocation.goofy.updateFactory(); |
| }; |
| |
| /** |
| * Sets iframe to fullscreen size. Also iframe gets higher z-index than |
| * test panel so it will cover all other stuffs in goofy. |
| * @export |
| * @param {boolean} enable fullscreen iframe or not. |
| */ |
| cros.factory.Test.prototype.setFullScreen = function(enable) { |
| goog.dom.classes.enable(this.invocation.iframe, 'goofy-test-fullscreen', |
| enable); |
| }; |
| |
| |
| /** |
| * UI for a single test invocation. |
| * @constructor |
| * @param {cros.factory.Goofy} goofy |
| * @param {string} path |
| */ |
| cros.factory.Invocation = function(goofy, path, uuid) { |
| /** |
| * Reference to the Goofy object. |
| * @type cros.factory.Goofy |
| */ |
| this.goofy = goofy; |
| |
| /** |
| * @type string |
| */ |
| this.path = path; |
| |
| /** |
| * UUID of the invocation. |
| * @type string |
| */ |
| this.uuid = uuid; |
| |
| /** |
| * Test API for the invocation. |
| */ |
| this.test = new cros.factory.Test(this); |
| |
| /** |
| * The iframe containing the test. |
| * @type HTMLIFrameElement |
| */ |
| this.iframe = goog.dom.iframe.createBlank(new goog.dom.DomHelper(document)); |
| goog.dom.classes.add(this.iframe, 'goofy-test-iframe'); |
| goog.dom.classes.enable(this.iframe, 'goofy-test-visible', |
| /** @type boolean */( |
| goofy.pathTestMap[path].state.visible)); |
| document.getElementById('goofy-main').appendChild(this.iframe); |
| this.iframe.contentWindow.$ = goog.bind(function(id) { |
| return this.iframe.contentDocument.getElementById(id); |
| }, this); |
| this.iframe.contentWindow.test = this.test; |
| this.iframe.contentWindow.focus(); |
| }; |
| |
| /** |
| * Returns state information for this invocation. |
| * @return Object |
| */ |
| cros.factory.Invocation.prototype.getState = function() { |
| return this.goofy.pathTestMap[this.path].state; |
| }; |
| |
| /** |
| * Disposes of the invocation (and destroys the iframe). |
| */ |
| cros.factory.Invocation.prototype.dispose = function() { |
| if (this.iframe) { |
| goog.dom.removeNode(this.iframe); |
| this.goofy.invocations[this.uuid] = null; |
| this.iframe = null; |
| } |
| }; |
| |
| /** |
| * The main Goofy UI. |
| * |
| * @constructor |
| */ |
| cros.factory.Goofy = function() { |
| /** |
| * The WebSocket we'll use to communicate with the backend. |
| * @type goog.net.WebSocket |
| */ |
| this.ws = new goog.net.WebSocket(); |
| |
| /** |
| * Whether we have opened the WebSocket yet. |
| * @type boolean |
| */ |
| this.wsOpened = false; |
| |
| /** |
| * The UUID that we received from Goofy when starting up. |
| * @type {?string} |
| */ |
| this.uuid = null; |
| |
| /** |
| * The currently visible context menu, if any. |
| * @type goog.ui.PopupMenu |
| */ |
| this.contextMenu = null; |
| |
| /** |
| * The last test for which a context menu was displayed. |
| * @type {?string} |
| */ |
| this.lastContextMenuPath = null; |
| |
| /** |
| * The time at which the last context menu was hidden. |
| * @type {?number} |
| */ |
| this.lastContextMenuHideTime = null; |
| |
| /** |
| * All tooltips that we have created. |
| * @type Array.<goog.ui.AdvancedTooltip> |
| */ |
| this.tooltips = []; |
| |
| /** |
| * The test tree. |
| */ |
| this.testTree = new goog.ui.tree.TreeControl('Tests'); |
| this.testTree.setShowRootNode(false); |
| this.testTree.setShowLines(false); |
| |
| /** |
| * A map from test path to the tree node for each test. |
| * @type Object.<string, goog.ui.tree.BaseNode> |
| */ |
| this.pathNodeMap = new Object(); |
| |
| /** |
| * A map from test path to the entry in the test list for that test. |
| * @type Object.<string, cros.factory.TestListEntry> |
| */ |
| this.pathTestMap = new Object(); |
| |
| |
| /** |
| * A map from test path to the tree node html id for external reference. |
| * @type Object.<string, string> |
| */ |
| this.pathNodeIdMap = new Object(); |
| |
| /** |
| * Whether Chinese mode is currently enabled. |
| * |
| * TODO(jsalz): Generalize this to multiple languages (but this isn't |
| * really necessary now). |
| * |
| * @type boolean |
| */ |
| this.zhMode = false; |
| |
| /** |
| * The tooltip for version number information. |
| */ |
| this.infoTooltip = new goog.ui.AdvancedTooltip( |
| document.getElementById('goofy-system-info-hover')); |
| this.infoTooltip.setHtml('Version information not yet available.'); |
| |
| /** |
| * UIs for individual test invocations (by UUID). |
| * @type Object.<string, cros.factory.Invocation> |
| */ |
| this.invocations = {}; |
| |
| /** |
| * Eng mode prompt. |
| * @type goog.ui.Dialog |
| */ |
| this.engineeringModeDialog = null; |
| |
| /** |
| * Shutdown prompt dialog. |
| * @type goog.ui.Dialog |
| */ |
| this.shutdownDialog = null; |
| |
| /** |
| * Visible dialogs. |
| * @type Array.<goog.ui.Dialog> |
| */ |
| this.dialogs = []; |
| |
| /** |
| * Whether eng mode is enabled. |
| * @type {boolean} |
| */ |
| this.engineeringMode = false; |
| |
| /** |
| * Last system info received. |
| * @type Object.<string, Object> |
| */ |
| this.systemInfo = {}; |
| |
| /** |
| * SHA1 hash of password to take UI out of operator mode. If |
| * null, eng mode is always enabled. Defaults to an invalid '?', |
| * which means that eng mode cannot be entered (will be set from |
| * Goofy's shared_data). |
| * @type {?string} |
| */ |
| this.engineeringPasswordSHA1 = '?'; |
| |
| /** |
| * Debug window. |
| * @type {goog.debug.FancyWindow} |
| */ |
| this.debugWindow = new goog.debug.FancyWindow('main'); |
| this.debugWindow.setEnabled(false); |
| this.debugWindow.init(); |
| |
| /** |
| * Key listener bound to this object. |
| */ |
| this.boundKeyListener = goog.bind(this.keyListener, this); |
| |
| // Set up magic keyboard shortcuts. |
| goog.events.listen( |
| window, goog.events.EventType.KEYDOWN, this.keyListener, true, this); |
| }; |
| |
| /** |
| * Sets the title of a modal dialog as HTML. |
| * @param {string} titleHTML |
| */ |
| cros.factory.Goofy.setDialogTitleHTML = function(dialog, titleHTML) { |
| goog.dom.getElementByClass( |
| 'modal-dialog-title-text', dialog.getElement()).innerHTML = titleHTML; |
| }; |
| |
| /** |
| * Event listener for Ctrl-Alt-keypress. |
| * @param {goog.events.KeyEvent} event |
| */ |
| cros.factory.Goofy.prototype.keyListener = function(event) { |
| if (event.altKey && event.ctrlKey) { |
| switch (String.fromCharCode(event.keyCode)) { |
| case '0': |
| if (!this.dialogs.length) { // If no dialogs are shown yet |
| this.promptEngineeringPassword(); |
| } |
| break; |
| case '1': |
| this.debugWindow.setEnabled(true); |
| break; |
| default: |
| // Nothing |
| } |
| } |
| // Disable shortcut Ctrl-Alt-* when not in engineering mode. |
| // Note: platformModifierKey == Command-key for Mac browser; |
| // for non-Mac browsers, it is Ctrl-key. |
| if (!this.engineeringMode && |
| event.altKey && event.platformModifierKey) { |
| event.stopPropagation(); |
| event.preventDefault(); |
| } |
| }; |
| |
| /** |
| * Initializes the split panes. |
| */ |
| cros.factory.Goofy.prototype.initSplitPanes = function() { |
| var viewportSize = goog.dom.getViewportSize(goog.dom.getWindow(document)); |
| var mainComponent = new goog.ui.Component(); |
| var consoleComponent = new goog.ui.Component(); |
| var mainAndConsole = new goog.ui.SplitPane( |
| mainComponent, consoleComponent, |
| goog.ui.SplitPane.Orientation.VERTICAL); |
| mainAndConsole.setInitialSize( |
| viewportSize.height - |
| Math.max(cros.factory.LOG_PANE_MIN_HEIGHT, |
| 1 - cros.factory.LOG_PANE_HEIGHT_FRACTION)); |
| |
| goog.debug.catchErrors(goog.bind(function(info) { |
| try { |
| this.logToConsole('JavaScript error (' + info.fileName + |
| ', line ' + info.line + '): ' + info.message, |
| 'goofy-internal-error'); |
| } catch (e) { |
| // Oof... error while logging an error! Maybe the DOM |
| // isn't set up properly yet; just ignore. |
| } |
| }, this), false); |
| |
| var controlComponent = new goog.ui.Component(); |
| var topSplitPane = new goog.ui.SplitPane( |
| controlComponent, mainAndConsole, |
| goog.ui.SplitPane.Orientation.HORIZONTAL); |
| topSplitPane.setInitialSize( |
| Math.max(cros.factory.CONTROL_PANEL_MIN_WIDTH, |
| viewportSize.width * |
| cros.factory.CONTROL_PANEL_WIDTH_FRACTION)); |
| // Decorate the uppermost splitpane and disable its context menu. |
| var topSplitPaneElement = document.getElementById('goofy-splitpane'); |
| topSplitPane.decorate(topSplitPaneElement); |
| // Disable context menu except in engineering mode. |
| goog.events.listen( |
| topSplitPaneElement, goog.events.EventType.CONTEXTMENU, |
| function(event) { |
| if (!this.engineeringMode) { |
| event.stopPropagation(); |
| event.preventDefault(); |
| } |
| }, |
| false, this); |
| |
| mainComponent.getElement().id = 'goofy-main'; |
| mainComponent.getElement().innerHTML = ( |
| '<img id="goofy-main-logo" src="images/logo256.png">'); |
| consoleComponent.getElement().id = 'goofy-console'; |
| this.console = consoleComponent.getElement(); |
| this.main = mainComponent.getElement(); |
| |
| var propagate = true; |
| goog.events.listen( |
| topSplitPane, goog.ui.Component.EventType.CHANGE, |
| function(event) { |
| if (!propagate) { |
| // Prevent infinite recursion |
| return; |
| } |
| |
| propagate = false; |
| mainAndConsole.setFirstComponentSize( |
| mainAndConsole.getFirstComponentSize()); |
| propagate = true; |
| |
| var rect = mainComponent.getElement().getBoundingClientRect(); |
| this.sendRpc('get_shared_data', ['ui_scale_factor'], |
| function(uiScaleFactor) { |
| this.sendRpc('set_shared_data', |
| ['test_widget_size', |
| [rect.width * uiScaleFactor, |
| rect.height * uiScaleFactor], |
| 'test_widget_position', |
| [rect.left * uiScaleFactor, |
| rect.top * uiScaleFactor]]); |
| }); |
| }, false, this); |
| mainAndConsole.setFirstComponentSize( |
| mainAndConsole.getFirstComponentSize()); |
| goog.events.listen( |
| window, goog.events.EventType.RESIZE, |
| function(event) { |
| topSplitPane.setSize( |
| goog.dom.getViewportSize(goog.dom.getWindow(document) || |
| window)); |
| }); |
| |
| // Whenever we get focus, try to focus any visible iframe (if no modal |
| // dialog is visible). |
| goog.events.listen( |
| window, goog.events.EventType.FOCUS, |
| function() { goog.Timer.callOnce(this.focusInvocation, 0, this); }, |
| false, this); |
| }; |
| |
| /** |
| * Returns focus to any visible invocation. |
| */ |
| cros.factory.Goofy.prototype.focusInvocation = function() { |
| if (goog.array.find(this.dialogs, function(dialog) { |
| return dialog.isVisible(); |
| })) { |
| // Don't divert focus, since a dialog is visible. |
| return; |
| } |
| |
| goog.object.forEach(this.invocations, function(i) { |
| if (i && i.iframe && /** @type boolean */( |
| i.getState().visible)) { |
| goog.Timer.callOnce(goog.bind(function() { |
| if (!this.contextMenu) { |
| i.iframe.focus(); |
| i.iframe.contentWindow.focus(); |
| } |
| }, this)); |
| } |
| }, this); |
| }; |
| |
| /** |
| * Initializes the WebSocket. |
| */ |
| cros.factory.Goofy.prototype.initWebSocket = function() { |
| goog.events.listen(this.ws, goog.net.WebSocket.EventType.OPENED, |
| function(event) { |
| this.logInternal('Connection to Goofy opened.'); |
| this.wsOpened = true; |
| }, false, this); |
| goog.events.listen(this.ws, goog.net.WebSocket.EventType.ERROR, |
| function(event) { |
| this.logInternal('Error connecting to Goofy.'); |
| }, false, this); |
| goog.events.listen(this.ws, goog.net.WebSocket.EventType.CLOSED, |
| function(event) { |
| if (this.wsOpened) { |
| this.logInternal('Connection to Goofy closed.'); |
| this.wsOpened = false; |
| } |
| }, false, this); |
| goog.events.listen(this.ws, goog.net.WebSocket.EventType.MESSAGE, |
| function(event) { |
| this.handleBackendEvent(event.message); |
| }, false, this); |
| window.setInterval(goog.bind(this.keepAlive, this), |
| cros.factory.KEEP_ALIVE_INTERVAL_MSEC); |
| window.setInterval(goog.bind(this.updateStatus, this), |
| cros.factory.SYSTEM_STATUS_INTERVAL_MSEC); |
| this.updateStatus(); |
| this.ws.open("ws://" + window.location.host + "/event"); |
| }; |
| |
| /** |
| * Starts the UI. |
| */ |
| cros.factory.Goofy.prototype.init = function() { |
| this.initLanguageSelector(); |
| this.initSplitPanes(); |
| |
| // Listen for keyboard shortcuts. |
| goog.events.listen( |
| window, goog.events.EventType.KEYDOWN, |
| function(event) { |
| if (event.altKey || event.ctrlKey) { |
| this.handleShortcut(String.fromCharCode(event.keyCode)); |
| } |
| }, false, this); |
| |
| this.initWebSocket(); |
| this.sendRpc('get_test_list', [], this.setTestList); |
| this.sendRpc('get_shared_data', ['system_info'], this.setSystemInfo); |
| this.sendRpc( |
| 'get_shared_data', ['test_list_options'], |
| function(options) { |
| this.engineeringPasswordSHA1 = |
| options['engineering_password_sha1']; |
| // If no password, enable eng mode, and don't |
| // show the 'disable' link, since there is no way to |
| // enable it. |
| goog.style.showElement(document.getElementById( |
| 'goofy-disable-engineering-mode'), |
| this.engineeringPasswordSHA1 != null); |
| this.setEngineeringMode(this.engineeringPasswordSHA1 == null); |
| }); |
| this.sendRpc( |
| 'get_shared_data', ['startup_error'], |
| function(error) { |
| this.alert( |
| cros.factory.Label( |
| ('An error occurred while starting ' + |
| 'the factory test system.<br>' + |
| 'Factory testing cannot proceed.'), |
| ('开工厂测试系统时发生错误.<br>' + |
| '没办法继续测试.')) + |
| '<div class="goofy-startup-error">' + |
| goog.string.htmlEscape(error) + |
| '</div>'); |
| }, |
| function() { |
| // Unable to retrieve the key; that's fine, no startup error! |
| }); |
| |
| var timer = new goog.Timer(1000); |
| goog.events.listen(timer, goog.Timer.TICK, this.updateTime, false, this); |
| timer.dispatchTick(); |
| timer.start(); |
| }; |
| |
| /** |
| * Sets up the language selector. |
| */ |
| cros.factory.Goofy.prototype.initLanguageSelector = function() { |
| goog.events.listen( |
| document.getElementById('goofy-language-selector'), |
| goog.events.EventType.CLICK, |
| function(event) { |
| this.zhMode = !this.zhMode; |
| this.updateCSSClasses(); |
| this.sendRpc('set_shared_data', |
| ['ui_lang', this.zhMode ? 'zh' : 'en']); |
| }, false, this); |
| |
| this.updateCSSClasses(); |
| this.sendRpc('get_shared_data', ['ui_lang'], function(lang) { |
| this.zhMode = lang == 'zh'; |
| this.updateCSSClasses(); |
| }); |
| }; |
| |
| /** |
| * Gets an invocation for a test (creating it if necessary). |
| * |
| * @param {string} path |
| * @param {string} invocationUuid |
| * @return the invocation, or null if the invocation has already been created |
| * and deleted. |
| */ |
| cros.factory.Goofy.prototype.getOrCreateInvocation = function( |
| path, invocationUuid) { |
| if (!(invocationUuid in this.invocations)) { |
| cros.factory.logger.info('Creating UI for test ' + path + |
| ' (invocation ' + invocationUuid); |
| this.invocations[invocationUuid] = |
| new cros.factory.Invocation(this, path, invocationUuid); |
| } |
| return this.invocations[invocationUuid]; |
| }; |
| |
| /** |
| * Updates language classes in a document based on the current value of |
| * zhMode. |
| */ |
| cros.factory.Goofy.prototype.updateCSSClassesInDocument = function(doc) { |
| if (doc.body) { |
| goog.dom.classes.enable(doc.body, 'goofy-lang-en', !this.zhMode); |
| goog.dom.classes.enable(doc.body, 'goofy-lang-zh', this.zhMode); |
| goog.dom.classes.enable(doc.body, 'goofy-engineering-mode', |
| this.engineeringMode); |
| goog.dom.classes.enable(doc.body, 'goofy-operator-mode', |
| !this.engineeringMode); |
| } |
| }; |
| |
| /** |
| * Updates language classes in the UI based on the current value of |
| * zhMode. |
| */ |
| cros.factory.Goofy.prototype.updateCSSClasses = function() { |
| this.updateCSSClassesInDocument.call(this, document); |
| goog.object.forEach(this.invocations, function(i) { |
| if (i && i.iframe) { |
| this.updateCSSClassesInDocument.call(this, |
| i.iframe.contentDocument); |
| } |
| }, this); |
| } |
| |
| /** |
| * Updates the system info tooltip. |
| * @param systemInfo Object.<string, string> |
| */ |
| cros.factory.Goofy.prototype.setSystemInfo = function(systemInfo) { |
| this.systemInfo = systemInfo; |
| |
| var table = []; |
| table.push('<table id="goofy-system-info">'); |
| goog.array.forEach(cros.factory.SYSTEM_INFO_LABELS, function(item) { |
| var value = systemInfo[item.key]; |
| var html; |
| if (item.transform) { |
| html = item.transform(value); |
| } else { |
| html = value == undefined ? |
| cros.factory.UNKNOWN_LABEL : |
| goog.string.htmlEscape(value); |
| } |
| table.push( |
| '<tr><th>' + item.label + '</th><td>' + html + |
| '</td></tr>'); |
| }); |
| table.push('<tr><th>' + |
| cros.factory.Label('System time', '系统时间') + |
| '</th><td id="goofy-time"></td></th></tr>'); |
| table.push('</table>'); |
| this.infoTooltip.setHtml(table.join('')); |
| this.updateTime(); |
| |
| goog.dom.classes.enable(document.body, 'goofy-update-available', |
| !!systemInfo['update_md5sum']); |
| }; |
| |
| /** |
| * Updates the current time. |
| */ |
| cros.factory.Goofy.prototype.updateTime = function() { |
| var element = document.getElementById('goofy-time'); |
| if (element) { |
| element.innerHTML = new goog.date.DateTime().toUTCIsoString(true) + |
| ' UTC'; |
| } |
| }; |
| |
| /** |
| * Registers a dialog. sets the dialog setDisposeOnHide to true, and |
| * returns focus to any running invocation when the dialog is |
| * hidden/disposed. |
| * |
| * @param {goog.ui.Dialog} dialog |
| */ |
| cros.factory.Goofy.prototype.registerDialog = function(dialog) { |
| this.dialogs.push(dialog); |
| dialog.setDisposeOnHide(true); |
| goog.events.listen(dialog, goog.ui.Component.EventType.SHOW, function() { |
| window.focus(); |
| // Hack: if the dialog contains an input element or |
| // button, focus it. (For instance, Prompt only calls |
| // select(), not focus(), on the text field, which causes |
| // ESC and Enter shortcuts not to work.) |
| var elt = dialog.getElement(); |
| var inputs = elt.getElementsByTagName('input'); |
| if (!inputs.length) { |
| inputs = elt.getElementsByTagName('button'); |
| } |
| if (inputs.length) { |
| inputs[0].focus(); |
| } |
| }, false, this); |
| goog.events.listen(dialog, goog.ui.Component.EventType.HIDE, function() { |
| goog.Timer.callOnce(this.focusInvocation, 0, this); |
| goog.array.remove(this.dialogs, dialog); |
| }, false, this); |
| }; |
| |
| /** |
| * Displays an alert. |
| * @param {string} messageHtml |
| */ |
| cros.factory.Goofy.prototype.alert = function(messageHtml) { |
| var dialog = new goog.ui.Dialog(); |
| this.registerDialog(dialog); |
| dialog.setTitle('Alert'); |
| dialog.setButtonSet(goog.ui.Dialog.ButtonSet.createOk()); |
| dialog.setContent(messageHtml); |
| dialog.setVisible(true); |
| goog.dom.classes.add(dialog.getElement(), 'goofy-alert'); |
| }; |
| |
| /** |
| * Centers an element over the console. |
| * @param {Element} element |
| */ |
| cros.factory.Goofy.prototype.positionOverConsole = function(element) { |
| var consoleBounds = goog.style.getBounds(this.console.parentNode); |
| var size = goog.style.getSize(element); |
| goog.style.setPosition( |
| element, |
| consoleBounds.left + consoleBounds.width/2 - size.width/2, |
| consoleBounds.top + consoleBounds.height/2 - size.height/2); |
| }; |
| |
| /** |
| * Prompts to enter eng mode. |
| */ |
| cros.factory.Goofy.prototype.promptEngineeringPassword = function() { |
| if (this.engineeringModeDialog) { |
| this.engineeringModeDialog.setVisible(false); |
| this.engineeringModeDialog.dispose(); |
| this.engineeringModeDialog = null; |
| } |
| if (!this.engineeringPasswordSHA1) { |
| this.alert('No password has been set.'); |
| return; |
| } |
| if (this.engineeringMode) { |
| this.setEngineeringMode(false); |
| return; |
| } |
| |
| this.engineeringModeDialog = new goog.ui.Prompt( |
| 'Password', '', |
| goog.bind(function(text) { |
| if (!text || text == '') { |
| return; |
| } |
| var hash = new goog.crypt.Sha1(); |
| hash.update(text); |
| var digest = goog.crypt.byteArrayToHex(hash.digest()); |
| if (digest == this.engineeringPasswordSHA1) { |
| this.setEngineeringMode(true); |
| } else { |
| this.alert('Incorrect password.'); |
| } |
| }, this)); |
| this.registerDialog(this.engineeringModeDialog); |
| this.engineeringModeDialog.setVisible(true); |
| goog.dom.classes.add(this.engineeringModeDialog.getElement(), |
| 'goofy-engineering-mode-dialog'); |
| this.engineeringModeDialog.reposition(); |
| this.positionOverConsole(this.engineeringModeDialog.getElement()); |
| }; |
| |
| /** |
| * Sets eng mode. |
| * @param {boolean} enabled |
| */ |
| cros.factory.Goofy.prototype.setEngineeringMode = function(enabled) { |
| this.engineeringMode = enabled; |
| this.updateCSSClasses(); |
| this.sendRpc('set_shared_data', ['engineering_mode', enabled]); |
| }; |
| |
| /** |
| * Deals with data about a pending reboot. |
| * @param {cros.factory.PendingShutdownEvent} shutdownInfo |
| */ |
| cros.factory.Goofy.prototype.setPendingShutdown = function(shutdownInfo) { |
| if (this.shutdownDialog) { |
| this.shutdownDialog.setVisible(false); |
| this.shutdownDialog.dispose(); |
| this.shutdownDialog = null; |
| } |
| if (!shutdownInfo || !shutdownInfo.time) { |
| return; |
| } |
| |
| var verbEn = shutdownInfo.operation == 'reboot' ? |
| 'Rebooting' : 'Shutting down'; |
| var verbZh = shutdownInfo.operation == 'reboot' ? '重开机' : '关机'; |
| |
| var timesEn = shutdownInfo.iterations == 1 ? 'once' : ( |
| shutdownInfo.iteration + ' of ' + shutdownInfo.iterations + ' times'); |
| var timesZh = shutdownInfo.iterations == 1 ? '1次' : ( |
| shutdownInfo.iterations + '次' + verbZh + '测试中的第' + |
| shutdownInfo.iteration + '次'); |
| |
| this.shutdownDialog = new goog.ui.Dialog(); |
| this.registerDialog(this.shutdownDialog); |
| this.shutdownDialog.setContent( |
| '<p>' + verbEn + ' in <span class="goofy-shutdown-secs"></span> ' + |
| 'second<span class="goofy-shutdown-secs-plural"></span> (' + timesEn + |
| ').<br>' + |
| 'To cancel, press the Escape key.</p>' + |
| '<p>将会在<span class="goofy-shutdown-secs"></span>秒内' + verbZh + |
| '(' + timesZh + ').<br>按ESC键取消.</p>'); |
| |
| var progressBar = new goog.ui.ProgressBar(); |
| progressBar.render(this.shutdownDialog.getContentElement()); |
| |
| function tick() { |
| var now = new Date().getTime() / 1000.0; |
| |
| var startTime = shutdownInfo.time - shutdownInfo.delay_secs; |
| var endTime = shutdownInfo.time; |
| var fraction = (now - startTime) / (endTime - startTime); |
| progressBar.setValue(goog.math.clamp(fraction, 0, 1) * 100); |
| |
| var secondsLeft = 1 + Math.floor(Math.max(0, endTime - now)); |
| goog.array.forEach( |
| goog.dom.getElementsByClass('goofy-shutdown-secs'), function(elt) { |
| elt.innerHTML = secondsLeft; |
| }, this); |
| goog.array.forEach( |
| goog.dom.getElementsByClass('goofy-shutdown-secs-plural'), |
| function(elt) { |
| elt.innerHTML = secondsLeft == 1 ? '' : 's'; |
| }, this); |
| } |
| |
| var timer = new goog.Timer(20); |
| goog.events.listen(timer, goog.Timer.TICK, tick, false, this); |
| timer.start(); |
| |
| goog.events.listen(this.shutdownDialog, |
| goog.ui.PopupBase.EventType.BEFORE_HIDE, |
| function(event) { |
| timer.dispose(); |
| }, false, this); |
| |
| function onKey(e) { |
| if (e.keyCode == goog.events.KeyCodes.ESC) { |
| this.sendEvent('goofy:cancel_shutdown', {}); |
| // Wait for Goofy to reset the pending_shutdown data. |
| } |
| } |
| goog.events.listen(this.shutdownDialog.getElement(), |
| goog.events.EventType.KEYDOWN, onKey, false, this); |
| |
| this.shutdownDialog.setButtonSet(null); |
| this.shutdownDialog.setHasTitleCloseButton(false); |
| this.shutdownDialog.setEscapeToCancel(false); |
| goog.dom.classes.add(this.shutdownDialog.getElement(), |
| 'goofy-shutdown-dialog'); |
| this.shutdownDialog.setVisible(true); |
| // The dialog has no close box or buttons, so focus is a little weird. |
| // If it does lose focus, return it to the dialog. |
| goog.events.listen( |
| this.shutdownDialog.getElement(), goog.events.EventType.BLUR, |
| function(event) { |
| goog.Timer.callOnce(goog.bind(this.shutdownDialog.focus, |
| this.shutdownDialog)); |
| }, false, this); |
| }; |
| |
| /** |
| * Handles a keyboard shortcut. |
| * @param {string} key the key that was depressed (e.g., 'a' for Alt-A). |
| */ |
| cros.factory.Goofy.prototype.handleShortcut = function(key) { |
| for (var path in this.pathTestMap) { |
| var test = this.pathTestMap[path]; |
| if (test.kbd_shortcut && |
| test.kbd_shortcut.toLowerCase() == key.toLowerCase()) { |
| this.sendEvent('goofy:restart_tests', {path: path}); |
| return; |
| } |
| } |
| }; |
| |
| /** |
| * Does "auto-run": run all tests that have not yet passed. |
| */ |
| cros.factory.Goofy.prototype.startAutoTest = function() { |
| this.sendEvent('goofy:run_tests_with_status', { |
| 'status': ['UNTESTED', 'ACTIVE', 'FAILED']}); |
| } |
| |
| /** |
| * Makes a menu item for a context-sensitive menu. |
| * |
| * TODO(jsalz): Figure out the correct logic for this and how to localize this. |
| * (Please just consider this a rough cut for now!) |
| * |
| * @param {string} verbEn the action in English. |
| * @param {string} verbZh the action in Chinese. |
| * @param {string} adjectiveEn a descriptive adjective for the tests (e.g., |
| * 'failed'). |
| * @param {string} adjectiveZh the adjective in Chinese. |
| * @param {number} count the number of tests. |
| * @param {cros.factory.TestListEntry} test the name of the root node containing |
| * the tests. |
| * @param {Object} handler the handler function (see goog.events.listen). |
| * @param {boolean=} opt_adjectiveAtEnd put the adjective at the end in English |
| * (e.g., tests that have *not passed*) |
| * @param {string=} opt_suffixEn a suffix in English (e.g., |
| * ' and continue testing') |
| * @param {string=} opt_suffixZh a suffix in Chinese (e.g., '並繼續') |
| */ |
| cros.factory.Goofy.prototype.makeMenuItem = function( |
| verbEn, verbZh, adjectiveEn, adjectiveZh, count, test, handler, |
| opt_adjectiveAtEnd, opt_suffixEn, opt_suffixZh) { |
| |
| var labelEn = verbEn + ' '; |
| var labelZh = verbZh; |
| if (!test.subtests.length) { |
| // Leaf node (there will always be both a label_en and label_zh) |
| labelEn += (opt_adjectiveAtEnd ? '' : adjectiveEn) + |
| ' test “' + test.label_en + '”'; |
| labelZh += adjectiveZh + '测试' + '「' + test.label_zh + '」'; |
| } else { |
| labelEn += count + ' ' + (opt_adjectiveAtEnd ? '' : adjectiveEn) + ' ' + |
| (count == 1 ? 'test' : 'tests'); |
| if (test.label_en) { |
| // Not the root node; include the name |
| labelEn += ' in "' + goog.string.htmlEscape(test.label_en) + '"'; |
| } |
| |
| labelZh += count + '个' + adjectiveZh; |
| if (test.label_zh) { |
| // Not the root node; include the name |
| labelZh += '在「' + goog.string.htmlEscape(test.label_zh) + '」里面'; |
| } |
| labelZh += '的测试'; |
| } |
| |
| if (opt_adjectiveAtEnd) { |
| labelEn += ' that ' + (count == 1 ? 'has' : 'have') + ' not passed'; |
| } |
| if (opt_suffixEn) { |
| labelEn += opt_suffixEn; |
| } |
| if (opt_suffixZh) { |
| labelZh += opt_suffixZh; |
| } |
| |
| var item = new goog.ui.MenuItem(cros.factory.Content(labelEn, labelZh)); |
| item.setEnabled(count != 0); |
| goog.events.listen(item, goog.ui.Component.EventType.ACTION, |
| handler, true, this); |
| return item; |
| }; |
| |
| /** |
| * Returns true if all tests in the test lists before a given test have been |
| * run. |
| * @param {cros.factory.TestListEntry} test |
| */ |
| cros.factory.Goofy.prototype.allTestsRunBefore = function(test) { |
| var root = this.pathTestMap['']; |
| |
| // Create a stack containing only the root node, and walk through |
| // it depth-first. (Use a stack rather than recursion since we |
| // want to be able to bail out easily when we hit 'test' or an |
| // incomplete test.) |
| var stack = [root]; |
| while (stack) { |
| var item = stack.pop(); |
| if (item == test) { |
| return true; |
| } |
| if (item.subtests.length) { |
| // Append elements in right-to-left order so we will |
| // examine them in the correct order. |
| var copy = goog.array.clone(item.subtests); |
| copy.reverse(); |
| goog.array.extend(stack, copy); |
| } else { |
| if (item.state.status == 'ACTIVE' || |
| item.state.status == 'UNTESTED') { |
| return false; |
| } |
| } |
| } |
| // We should never reach this, since it means that we never saw |
| // test while iterating! |
| throw Error('Test not in test list'); |
| }; |
| |
| /** |
| * Displays a context menu for a test in the test tree. |
| * @param {string} path the path of the test whose context menu should be |
| * displayed. |
| * @param {Element} labelElement the label element of the node in the test |
| * tree. |
| * @param {Array.<goog.ui.Control>=} extraItems items to prepend to the |
| * menu. |
| */ |
| cros.factory.Goofy.prototype.showTestPopup = function(path, labelElement, |
| extraItems) { |
| var test = this.pathTestMap[path]; |
| |
| if (path == this.lastContextMenuPath && |
| (goog.now() - this.lastContextMenuHideTime < |
| goog.ui.PopupBase.DEBOUNCE_DELAY_MS)) { |
| // We just hid it; don't reshow. |
| return false; |
| } |
| |
| // If it's a leaf node, and it's the active but not the visible |
| // test, ask the backend to make it visible. |
| if (test.state.status == 'ACTIVE' && |
| !/** @type boolean */(test.state.visible) && |
| !test.subtests.length) { |
| this.sendEvent('goofy:set_visible_test', {'path': path}); |
| } |
| |
| // Hide all tooltips so that they don't fight with the context menu. |
| goog.array.forEach(this.tooltips, function(tooltip) { |
| tooltip.setVisible(false); |
| }); |
| |
| var menu = this.contextMenu = new goog.ui.PopupMenu(); |
| function addSeparator() { |
| if (menu.getChildCount() && |
| !(menu.getChildAt(menu.getChildCount() - 1) |
| instanceof goog.ui.MenuSeparator)) { |
| menu.addChild(new goog.ui.MenuSeparator(), true); |
| } |
| } |
| |
| this.lastContextMenuPath = path; |
| |
| var numLeaves = 0; |
| var numLeavesByStatus = {}; |
| var allPaths = []; |
| var activeAndDisableAbort = false; |
| function countLeaves(test) { |
| allPaths.push(test.path); |
| goog.array.forEach(test.subtests, function(subtest) { |
| countLeaves(subtest); |
| }, this); |
| |
| if (!test.subtests.length) { |
| ++numLeaves; |
| numLeavesByStatus[test.state.status] = 1 + ( |
| numLeavesByStatus[test.state.status] || 0); |
| // If there is any subtest that is active and can not be aborted, |
| // this test can not be aborted. |
| if (test.state.status == 'ACTIVE' && test.disable_abort) { |
| activeAndDisableAbort = true; |
| } |
| } |
| } |
| countLeaves(test); |
| |
| if (!this.engineeringMode && !this.allTestsRunBefore(test)) { |
| var item = new goog.ui.MenuItem(cros.factory.Content( |
| 'Not in engineering mode; cannot skip tests', |
| '工程模式才能跳过测试')); |
| menu.addChild(item, true); |
| item.setEnabled(false); |
| } else { |
| var allUntested = numLeavesByStatus['UNTESTED'] == numLeaves; |
| var restartOrRunEn = allUntested ? 'Run' : 'Restart'; |
| var restartOrRunZh = allUntested ? '执行' : '重跑'; |
| if (numLeaves > 1) { |
| restartOrRunEn += ' all'; |
| restartOrRunZh += '所有的'; |
| } |
| if (this.engineeringMode || |
| (!test.subtests.length && test.state.status != 'PASSED')) { |
| // Allow user to restart all tests under a particular node if |
| // (a) in engineering mode, or (b) if this is a single non-passed |
| // test. If neither of these is true, it's too easy to |
| // accidentally re-run a bunch of tests and wipe their state. |
| menu.addChild(this.makeMenuItem( |
| restartOrRunEn, restartOrRunZh, '', '', numLeaves, test, |
| function(event) { |
| this.sendEvent('goofy:restart_tests', {'path': path}); |
| }), true); |
| } |
| if (test.subtests.length) { |
| // Only show for parents. |
| menu.addChild(this.makeMenuItem( |
| 'Restart', '重跑', 'not passed', '未成功', |
| (numLeavesByStatus['UNTESTED'] || 0) + |
| (numLeavesByStatus['ACTIVE'] || 0) + |
| (numLeavesByStatus['FAILED'] || 0), |
| test, function(event) { |
| this.sendEvent('goofy:run_tests_with_status', { |
| 'status': ['UNTESTED', 'ACTIVE', 'FAILED'], |
| 'path': path |
| }); |
| }, /*opt_adjectiveAtEnd=*/true), true); |
| if (this.engineeringmode) { |
| // For operators, the previous menu item is |
| // sufficient. |
| menu.addChild(this.makeMenuItem( |
| 'Run', '执行', 'untested', '未测的', |
| (numLeavesByStatus['UNTESTED'] || 0) + |
| (numLeavesByStatus['ACTIVE'] || 0), |
| test, function(event) { |
| this.sendEvent('goofy:auto_run', {'path': path}); |
| }), true); |
| } |
| } |
| } |
| addSeparator(); |
| |
| var stopAllItem = new goog.ui.MenuItem(cros.factory.Content( |
| 'Stop all tests', |
| '停止所有的测试')); |
| stopAllItem.setEnabled(numLeavesByStatus['ACTIVE'] > 0); |
| menu.addChild(stopAllItem, true); |
| goog.events.listen( |
| stopAllItem, goog.ui.Component.EventType.ACTION, |
| function(event) { |
| this.sendEvent('goofy:stop', {'fail': true}); |
| }, true, this); |
| |
| // When there is any active test, enable abort item in menu |
| // if goofy is in engineering mode or there is no |
| // active subtest with disable_abort=true. |
| if (numLeavesByStatus['ACTIVE'] && |
| (this.engineeringMode || !activeAndDisableAbort)) { |
| menu.addChild(this.makeMenuItem( |
| 'Abort', '取消', 'active', '執行中的', |
| numLeavesByStatus['ACTIVE'] || 0, |
| test, function(event) { |
| this.sendEvent('goofy:stop', {'path': path, 'fail': true}); |
| }, false, ' and continue testing', '並繼續'), true); |
| } |
| |
| if (this.engineeringMode && !test.subtests.length) { |
| addSeparator(); |
| menu.addChild(this.createViewLogMenu(path), true); |
| } |
| |
| if (extraItems && extraItems.length) { |
| addSeparator(); |
| goog.array.forEach(extraItems, function(item) { |
| menu.addChild(item, true); |
| }, this); |
| } |
| |
| menu.render(document.body); |
| menu.showAtElement(labelElement, |
| goog.positioning.Corner.BOTTOM_LEFT, |
| goog.positioning.Corner.TOP_LEFT); |
| goog.events.listen(menu, goog.ui.Component.EventType.HIDE, |
| function(event) { |
| menu.dispose(); |
| this.contextMenu = null; |
| this.lastContextMenuHideTime = goog.now(); |
| // Return focus to visible test, if any. |
| this.focusInvocation(); |
| }, true, this); |
| return true; |
| }; |
| |
| cros.factory.Goofy.prototype.HMS_TIME_FORMAT = |
| new goog.i18n.DateTimeFormat('HH:mm:ss'); |
| /** |
| * Returns a "View logs" submenu for a given test path. |
| * @param path string |
| * @return goog.ui.SubMenu |
| */ |
| cros.factory.Goofy.prototype.createViewLogMenu = function(path) { |
| var subMenu = new goog.ui.SubMenu('View logs'); |
| var loadingItem = new goog.ui.MenuItem('Loading...'); |
| loadingItem.setEnabled(false); |
| subMenu.addItem(loadingItem); |
| |
| this.sendRpc('get_test_history', [path], function(history) { |
| if (!subMenu.isVisible()) { |
| // e.g., menu already went away |
| return; |
| } |
| |
| if (!history.length) { |
| loadingItem.setCaption('No logs available'); |
| return; |
| } |
| |
| if (subMenu.indexOfChild(loadingItem) >= 0) { |
| subMenu.removeItem(loadingItem); |
| } |
| |
| // Arrange in descending order of time (it is returned in |
| // ascending order). |
| history.reverse(); |
| |
| var count = history.length; |
| goog.array.forEach(history, function(entry) { |
| var status = entry.status ? entry.status.toLowerCase() : |
| 'started'; |
| var title = count-- + '. Run at '; |
| |
| if (entry.init_time) { |
| // TODO(jsalz): Localize (but not that important since this |
| // is not for operators) |
| |
| title += this.HMS_TIME_FORMAT.format( |
| new Date(entry.init_time * 1000)); |
| } |
| title += ' (' + status; |
| |
| var time = /** @type number */(entry.end_time) || |
| entry.init_time; |
| if (time) { |
| var secondsAgo = goog.now() / 1000.0 - time; |
| |
| var hoursAgo = Math.floor(secondsAgo / 3600); |
| secondsAgo -= hoursAgo * 3600; |
| |
| var minutesAgo = Math.floor(secondsAgo / 60); |
| secondsAgo -= minutesAgo * 60; |
| |
| title += ' '; |
| if (hoursAgo) { |
| title += hoursAgo + ' h '; |
| } |
| if (minutesAgo) { |
| title += minutesAgo + ' m '; |
| } |
| title += Math.floor(secondsAgo) + ' s ago'; |
| } |
| title += ')…'; |
| |
| var item = new goog.ui.MenuItem( |
| goog.dom.createDom('span', |
| 'goofy-view-logs-status-' + status, |
| title)); |
| goog.events.listen( |
| item, goog.ui.Component.EventType.ACTION, |
| function(event) { |
| this.showHistoryEntry( |
| entry.path, |
| /** @type string */(entry.invocation)); |
| }, false, this); |
| |
| subMenu.addItem(item); |
| }, this); |
| }); |
| |
| return subMenu; |
| }; |
| |
| /** |
| * Displays a dialog containing logs. |
| * @param {string} titleHTML |
| * @param {string} data text to show in the dialog. |
| */ |
| cros.factory.Goofy.prototype.showLogDialog = function(titleHTML, data) { |
| var dialog = new goog.ui.Dialog(); |
| this.registerDialog(dialog); |
| dialog.setModal(false); |
| |
| var viewSize = goog.dom.getViewportSize( |
| goog.dom.getWindow(document) || window); |
| var maxWidth = viewSize.width * cros.factory.MAX_DIALOG_SIZE_FRACTION; |
| var maxHeight = viewSize.height * cros.factory.MAX_DIALOG_SIZE_FRACTION; |
| |
| dialog.setContent('<div class="goofy-log-data"' + |
| ' style="max-width: ' + maxWidth + |
| '; max-height: ' + maxHeight + '">' + |
| goog.string.htmlEscape(data) + |
| '</div>' + |
| '<div class="goofy-log-time"></div>'); |
| dialog.setButtonSet(goog.ui.Dialog.ButtonSet.createOk()); |
| dialog.setVisible(true); |
| cros.factory.Goofy.setDialogTitleHTML(dialog, titleHTML); |
| |
| var logDataElement = goog.dom.getElementByClass('goofy-log-data', |
| dialog.getContentElement()); |
| logDataElement.scrollTop = logDataElement.scrollHeight; |
| |
| var logTimeElement = goog.dom.getElementByClass('goofy-log-time', |
| dialog.getContentElement()); |
| var timer = new goog.Timer(1000); |
| goog.events.listen(timer, goog.Timer.TICK, function(event) { |
| // Show time in the same format as in the logs |
| logTimeElement.innerHTML = ( |
| cros.factory.Label('System time: ', |
| '系统时间:') + |
| new goog.date.DateTime().toUTCIsoString(true, true). |
| replace(' ', 'T')); |
| }, false, this); |
| timer.dispatchTick(); |
| timer.start(); |
| goog.events.listen(dialog, goog.ui.Component.EventType.HIDE, |
| function(event) { |
| timer.dispose(); |
| }, false, this); |
| }; |
| |
| |
| /** |
| * Displays a dialog containing the contents of /var/log/messages. |
| */ |
| cros.factory.Goofy.prototype.viewVarLogMessages = function() { |
| this.sendRpc( |
| 'GetVarLogMessages', [], |
| function(data) { |
| this.showLogDialog('/var/log/messages', data); |
| }); |
| }; |
| |
| /** |
| * Displays a dialog containing the contents of /var/log/messages |
| * before the last reboot. |
| */ |
| cros.factory.Goofy.prototype.viewVarLogMessagesBeforeReboot = function() { |
| this.sendRpc( |
| 'GetVarLogMessagesBeforeReboot', [], |
| function(data) { |
| data = data || 'Unable to find log message indicating reboot.'; |
| this.showLogDialog( |
| cros.factory.Label('/var/log/messages before last reboot', |
| '上次重开机前的 /var/log/messages'), |
| data); |
| }); |
| }; |
| |
| /** |
| * Displays a dialog containing the contents of dmesg. |
| */ |
| cros.factory.Goofy.prototype.viewDmesg = function() { |
| this.sendRpc( |
| 'GetDmesg', [], |
| function(data) { |
| this.showLogDialog('dmesg', data); |
| }); |
| }; |
| |
| /** |
| * Saves factory logs to a USB drive. |
| */ |
| cros.factory.Goofy.prototype.saveFactoryLogsToUSB = function() { |
| var titleContent = cros.factory.Content( |
| 'Save Factory Logs to USB', '保存工厂记录到 U盘'); |
| |
| function doSave() { |
| function callback(id) { |
| if (id == null) { |
| // Cancelled. |
| return; |
| } |
| |
| var dialog = new goog.ui.Dialog(); |
| this.registerDialog(dialog); |
| dialog.setTitle(titleContent); |
| dialog.setContent( |
| cros.factory.Label('Saving factory logs to USB drive...', |
| '正在保存工厂记录到 U盘...')); |
| dialog.setButtonSet(null); |
| dialog.setVisible(true); |
| this.positionOverConsole(dialog.getElement()); |
| this.sendRpc('SaveLogsToUSB', [id], |
| function(info) { |
| var dev = info[0]; |
| var filename = info[1]; |
| var size = info[2]; |
| var temporary = info[3]; |
| |
| dialog.setContent( |
| cros.factory.Label( |
| 'Success! Saved factory logs (' + size + |
| ' bytes) to ' + dev + ' as<br>' + filename + '.' + |
| (temporary ? ' The drive has been unmounted.' : ''), |
| '保存工厂记录 (' + size + |
| ' bytes) 到 U盘 ' + |
| dev + ' 已成功,文件叫<br>' + |
| filename + '。' + |
| (temporary ? 'U盘已卸载。' : ''))); |
| dialog.setButtonSet(goog.ui.Dialog.ButtonSet.createOk()); |
| this.positionOverConsole(dialog.getElement()); |
| }, function(response) { |
| dialog.setContent( |
| 'Unable to save logs: ' + |
| goog.string.htmlEscape(response.error.message)); |
| dialog.setButtonSet(goog.ui.Dialog.ButtonSet.createOk()); |
| this.positionOverConsole(dialog.getElement()); |
| }); |
| } |
| |
| var idDialog = new goog.ui.Prompt( |
| titleContent, |
| cros.factory.Label( |
| 'Enter an optional identifier for the archive ' + |
| '(or press Enter for none):', |
| '请输入识別号给工厂记录文件,' + |
| '或按回车键不选:'), |
| goog.bind(callback, this)); |
| this.registerDialog(idDialog); |
| idDialog.setVisible(true); |
| goog.dom.classes.add(idDialog.getElement(), |
| 'goofy-log-identifier-prompt'); |
| this.positionOverConsole(idDialog.getElement()); |
| } |
| |
| // Active timer, if any. |
| var timer = null; |
| |
| var waitForUSBDialog = new goog.ui.Dialog(); |
| this.registerDialog(waitForUSBDialog); |
| waitForUSBDialog.setContent( |
| cros.factory.Label('Please insert a formatted USB stick<br>' + |
| 'and wait a moment for it to be mounted.', |
| '请插入 U盘后稍等掛载。')); |
| waitForUSBDialog.setButtonSet( |
| new goog.ui.Dialog.ButtonSet(). |
| addButton(goog.ui.Dialog.ButtonSet.DefaultButtons.CANCEL, |
| false, true)); |
| waitForUSBDialog.setTitle(titleContent); |
| |
| function waitForUSB() { |
| function restartWaitForUSB() { |
| waitForUSBDialog.setVisible(true); |
| this.positionOverConsole(waitForUSBDialog.getElement()); |
| timer = goog.Timer.callOnce(goog.bind(waitForUSB, this), |
| cros.factory.MOUNT_USB_DELAY_MSEC); |
| } |
| this.sendRpc( |
| 'IsUSBDriveAvailable', [], |
| function(available) { |
| if (available) { |
| waitForUSBDialog.dispose(); |
| doSave.call(this); |
| } else { |
| restartWaitForUSB.call(this); |
| } |
| }, goog.bind(restartWaitForUSB, this)); |
| } |
| goog.events.listen(waitForUSBDialog, goog.ui.Component.EventType.HIDE, |
| function(event) { |
| if (timer) { |
| goog.Timer.clear(timer); |
| } |
| }, false, this); |
| waitForUSB.call(this); |
| }; |
| |
| cros.factory.Goofy.prototype.FULL_TIME_FORMAT = |
| new goog.i18n.DateTimeFormat('yyyy-MM-dd HH:mm:ss.SSS'); |
| /** |
| * Displays a dialog containing history for a given test invocation. |
| * @param {string} path |
| * @param {string} invocation |
| */ |
| cros.factory.Goofy.prototype.showHistoryEntry = function(path, invocation) { |
| this.sendRpc( |
| 'get_test_history_entry', [path, invocation], |
| function(data) { |
| var metadata = /** @type Object */(data.metadata); |
| var log = /** @type string */(data.log); |
| |
| var viewSize = goog.dom.getViewportSize( |
| goog.dom.getWindow(document) || window); |
| var maxWidth = viewSize.width * |
| cros.factory.MAX_DIALOG_SIZE_FRACTION; |
| var maxHeight = viewSize.height * |
| cros.factory.MAX_DIALOG_SIZE_FRACTION; |
| |
| var metadataTable = []; |
| metadataTable.push('<table class="goofy-history-metadata>"'); |
| goog.array.forEach( |
| [['status', 'Status'], |
| ['init_time', 'Creation time'], |
| ['start_time', 'Start time'], |
| ['end_time', 'End time']], |
| function(f) { |
| var name = f[0]; |
| var title = f[1]; |
| |
| if (metadata[name]) { |
| var value = metadata[name]; |
| delete metadata[name]; |
| if (goog.string.endsWith(name, '_time')) { |
| value = this.FULL_TIME_FORMAT.format( |
| new Date(value * 1000)); |
| } |
| metadataTable.push( |
| '<tr><th>' + title + '</th><td>' + |
| goog.string.htmlEscape(value) + |
| '</td></tr>'); |
| } |
| }, this); |
| |
| var keys = goog.object.getKeys(metadata); |
| keys.sort(); |
| goog.array.forEach(keys, function(key) { |
| if (key == 'log_tail') { |
| // Skip log_tail, since we already have the |
| // entire log. |
| return; |
| } |
| metadataTable.push('<tr><th>' + key + '</th><td>' + |
| goog.string.htmlEscape(metadata[key]) + |
| '</td></tr>'); |
| }, this); |
| |
| metadataTable.push('</table>'); |
| |
| var dialog = new goog.ui.Dialog(); |
| this.registerDialog(dialog); |
| dialog.setTitle(metadata.path + |
| ' (invocation ' + metadata.invocation + ')'); |
| dialog.setModal(false); |
| dialog.setContent( |
| '<div class="goofy-history" style="max-width: ' + |
| maxWidth + '; max-height: ' + maxHeight + '">' + |
| '<div class=goofy-history-header>Test Info</div>' + |
| metadataTable.join('') + |
| '<div class=goofy-history-header>Log</div>' + |
| '<div class=goofy-history-log>' + |
| goog.string.htmlEscape(log) + |
| '</div>' + |
| '</div>'); |
| dialog.setButtonSet(goog.ui.Dialog.ButtonSet.createOk()); |
| dialog.setVisible(true); |
| }); |
| }; |
| |
| /** |
| * Updates the tooltip for a test based on its status. |
| * The tooltip will be displayed only for failed tests. |
| * @param {string} path |
| * @param {goog.ui.AdvancedTooltip} tooltip |
| * @param {goog.events.Event} event the BEFORE_SHOW event that will cause the |
| * tooltip to be displayed. |
| */ |
| cros.factory.Goofy.prototype.updateTestToolTip = |
| function(path, tooltip, event) { |
| var test = this.pathTestMap[path]; |
| |
| tooltip.setHtml('') |
| |
| var errorMsg = test.state['error_msg']; |
| if (test.state.status != 'FAILED' || this.contextMenu || !errorMsg) { |
| // Don't bother showing it. |
| event.preventDefault(); |
| } else { |
| // Show the last failure. |
| var lines = errorMsg.split('\n'); |
| var html = ('Failure in "' + test.label_en + '":' + |
| '<div class="goofy-test-failure">' + |
| goog.string.htmlEscape(lines.shift()) + '</span>'); |
| |
| if (lines.length) { |
| html += ('<div class="goofy-test-failure-detail-link">' + |
| 'Show more detail...</div>' + |
| '<div class="goofy-test-failure-detail">' + |
| goog.string.htmlEscape(lines.join('\n')) + '</div>'); |
| } |
| if (test.state.invocation) { |
| html += ('<div class="goofy-test-failure-view-log-link">' + |
| 'View log...</div>') |
| } |
| |
| tooltip.setHtml(html); |
| |
| if (lines.length) { |
| var link = goog.dom.getElementByClass( |
| 'goofy-test-failure-detail-link', tooltip.getElement()); |
| goog.events.listen( |
| link, goog.events.EventType.CLICK, |
| function(event) { |
| goog.dom.classes.add(tooltip.getElement(), |
| 'goofy-test-failure-expanded'); |
| tooltip.reposition(); |
| }, true, this); |
| } |
| if (test.state.invocation) { |
| var link = goog.dom.getElementByClass( |
| 'goofy-test-failure-view-log-link', tooltip.getElement()); |
| goog.events.listen( |
| link, goog.events.EventType.CLICK, |
| function(event) { |
| tooltip.dispose(); |
| this.showHistoryEntry(test.path, test.state.invocation); |
| }, false, this); |
| } |
| } |
| }; |
| |
| /** |
| * Sets up the UI for a the test list. (Should be invoked only once, when |
| * the test list is received.) |
| * @param {cros.factory.TestListEntry} testList the test list (the return value |
| * of the get_test_list RPC call). |
| */ |
| cros.factory.Goofy.prototype.setTestList = function(testList) { |
| cros.factory.logger.info('Received test list: ' + |
| goog.debug.expose(testList)); |
| goog.style.showElement(document.getElementById('goofy-loading'), false); |
| |
| this.addToNode(null, testList); |
| // expandAll is necessary to get all the elements to actually be |
| // created right away so we can add listeners. We'll collapse it later. |
| this.testTree.expandAll(); |
| this.testTree.render(document.getElementById('goofy-test-tree')); |
| |
| var addListener = goog.bind(function(path, labelElement, rowElement) { |
| var tooltip = new goog.ui.AdvancedTooltip(rowElement); |
| tooltip.setHideDelayMs(1000); |
| this.tooltips.push(tooltip); |
| goog.events.listen( |
| tooltip, goog.ui.Component.EventType.BEFORE_SHOW, |
| function(event) { |
| this.updateTestToolTip(path, tooltip, event); |
| }, true, this); |
| goog.events.listen( |
| rowElement, goog.events.EventType.CONTEXTMENU, |
| function(event) { |
| if (event.ctrlKey) { |
| // Ignore; let the default (browser) context menu |
| // show up. |
| return; |
| } |
| |
| this.showTestPopup(path, labelElement); |
| event.stopPropagation(); |
| event.preventDefault(); |
| }, true, this); |
| goog.events.listen( |
| labelElement, goog.events.EventType.MOUSEDOWN, |
| function(event) { |
| if (event.button == 0) { |
| this.showTestPopup(path, labelElement); |
| event.stopPropagation(); |
| event.preventDefault(); |
| } |
| }, true, this); |
| }, this); |
| |
| for (var path in this.pathNodeMap) { |
| var node = this.pathNodeMap[path]; |
| addListener(path, node.getLabelElement(), node.getRowElement()); |
| } |
| |
| goog.array.forEach([goog.events.EventType.MOUSEDOWN, |
| goog.events.EventType.CONTEXTMENU], |
| function(eventType) { |
| goog.events.listen( |
| document.getElementById('goofy-title'), |
| eventType, |
| function(event) { |
| if (eventType == goog.events.EventType.MOUSEDOWN && |
| event.button != 0) { |
| // Only process primary button for MOUSEDOWN. |
| return; |
| } |
| if (event.ctrlKey) { |
| // Ignore; let the default (browser) context menu |
| // show up. |
| return; |
| } |
| |
| var extraItems = []; |
| var addExtraItem = goog.bind( |
| function(labelEn, labelZh, action) { |
| var item = new goog.ui.MenuItem( |
| cros.factory.Content(labelEn, labelZh)); |
| goog.events.listen( |
| item, |
| goog.ui.Component.EventType.ACTION, |
| action, false, this); |
| extraItems.push(item); |
| }, this); |
| |
| if (this.engineeringMode) { |
| addExtraItem('Update factory software', |
| '更新工厂软体', |
| this.updateFactory); |
| extraItems.push(new goog.ui.MenuSeparator()); |
| addExtraItem('View /var/log/messages', |
| '检视 /var/log/messages', |
| this.viewVarLogMessages); |
| addExtraItem('View /var/log/messages ' + |
| 'before last reboot', |
| '检视上次重开机前的 ' + |
| '/var/log/messages', |
| this.viewVarLogMessagesBeforeReboot); |
| addExtraItem('View dmesg', '检视 dmesg', |
| this.viewDmesg); |
| } |
| |
| addExtraItem('Save factory logs to USB drive...', |
| '保存工厂记录到 U盘', |
| this.saveFactoryLogsToUSB); |
| |
| this.showTestPopup( |
| '', document.getElementById('goofy-logo-text'), |
| extraItems); |
| |
| event.stopPropagation(); |
| event.preventDefault(); |
| }, true, this); |
| }, this); |
| |
| this.testTree.collapseAll(); |
| this.sendRpc('get_test_states', [], function(stateMap) { |
| for (var path in stateMap) { |
| if (!goog.string.startsWith(path, "_")) { // e.g., __jsonclass__ |
| this.setTestState(path, stateMap[path]); |
| } |
| } |
| }); |
| }; |
| |
| /** |
| * Sends an event to update factory software. |
| * @export |
| */ |
| cros.factory.Goofy.prototype.updateFactory = function() { |
| var dialog = new goog.ui.Dialog(); |
| this.registerDialog(dialog); |
| dialog.setHasTitleCloseButton(false); |
| dialog.setContent( |
| cros.factory.Label('Updating factory software. Please wait...', |
| '正在更新工厂软体,请稍等...')); |
| dialog.setButtonSet(null); |
| dialog.setVisible(true); |
| cros.factory.Goofy.setDialogTitleHTML( |
| dialog, |
| cros.factory.Label('Software update', '更新工厂软体')); |
| dialog.reposition(); |
| |
| this.sendRpc( |
| 'UpdateFactory', [], function(ret) { |
| var success = ret[0]; |
| var updated = ret[1]; |
| var restartTime = ret[2]; |
| var errorMsg = ret[3]; |
| |
| if (updated) { |
| dialog.setTitle('Update succeeded'); |
| dialog.setContent( |
| cros.factory.Label( |
| 'Update succeeded. Restarting.', |
| '更新已成功,' + |
| '将会在几秒钟之内重新启动。')); |
| } else if (success) { // but not updated |
| dialog.setContent(cros.factory.Label( |
| 'No update is currently necessary.', |
| '目前不用更新工厂软体')); |
| dialog.setButtonSet(goog.ui.Dialog.ButtonSet.createOk()); |
| } else { |
| dialog.setContent( |
| cros.factory.Label('Update failed:', |
| '更新失败了:') + |
| '<pre>' + goog.string.htmlEscape(errorMsg) + '</pre>'); |
| dialog.setButtonSet(goog.ui.Dialog.ButtonSet.createOk()); |
| } |
| dialog.reposition(); |
| }); |
| }; |
| |
| /** |
| * Sets the state for a particular test. |
| * @param {string} path |
| * @param {Object.<string, Object>} state the TestState object (contained in |
| * an event or as a response to the RPC call). |
| */ |
| cros.factory.Goofy.prototype.setTestState = function(path, state) { |
| var node = this.pathNodeMap[path]; |
| if (!node) { |
| cros.factory.logger.warning('No node found for test path ' + path); |
| return; |
| } |
| |
| var elt = this.pathNodeMap[path].getElement(); |
| var test = this.pathTestMap[path]; |
| test.state = state; |
| |
| // Assign the appropriate class to the node, and remove all other |
| // status classes. |
| goog.dom.classes.addRemove( |
| elt, |
| goog.array.filter( |
| goog.dom.classes.get(elt), |
| function(cls) { |
| return goog.string.startsWith(cls, "goofy-status-") && cls |
| }), |
| 'goofy-status-' + state.status.toLowerCase()); |
| |
| goog.dom.classes.enable(elt, 'goofy-skip', |
| /** @type boolean */(state.skip)); |
| |
| var visible = /** @type boolean */(state.visible); |
| goog.dom.classes.enable(elt, 'goofy-test-visible', visible); |
| goog.object.forEach(this.invocations, function(invoc, uuid) { |
| if (invoc && invoc.path == path) { |
| goog.dom.classes.enable(invoc.iframe, |
| 'goofy-test-visible', visible); |
| if (visible) { |
| this.iframe.contentWindow.focus(); |
| } |
| } |
| }, this); |
| |
| if (state.status == 'ACTIVE') { |
| // Automatically show the test if it is running. |
| node.reveal(); |
| } else if (cros.factory.AUTO_COLLAPSE) { |
| // If collapsible, then collapse it in 250ms if still inactive. |
| if (node.getChildCount() != 0) { |
| window.setTimeout(function(event) { |
| if (test.state.status != 'ACTIVE') { |
| node.collapse(); |
| } |
| }, 250); |
| } |
| } |
| }; |
| |
| /** |
| * Adds a test node to the tree. |
| * |
| * Also normalizes the test node by adding label_zh if not present. (The root |
| * node will have neither label.) |
| * |
| * @param {goog.ui.tree.BaseNode} parent |
| * @param {cros.factory.TestListEntry} test |
| */ |
| cros.factory.Goofy.prototype.addToNode = function(parent, test) { |
| var node; |
| if (parent == null) { |
| node = this.testTree; |
| } else { |
| test.label_zh = test.label_zh || test.label_en; |
| |
| var label = '<span class="goofy-label-en">' + |
| goog.string.htmlEscape(test.label_en) + '</span>'; |
| label += '<span class="goofy-label-zh">' + |
| goog.string.htmlEscape(test.label_zh) + '</span>'; |
| if (test.kbd_shortcut) { |
| label = '<span class="goofy-kbd-shortcut">Alt-' + |
| goog.string.htmlEscape(test.kbd_shortcut.toUpperCase()) + |
| '</span>' + label; |
| } |
| node = this.testTree.createNode(label); |
| parent.addChild(node); |
| } |
| goog.array.forEach(test.subtests, function(subtest) { |
| this.addToNode(node, subtest); |
| }, this); |
| |
| node.setIconClass('goofy-test-icon'); |
| node.setExpandedIconClass('goofy-test-icon'); |
| |
| this.pathNodeMap[test.path] = node; |
| this.pathTestMap[test.path] = test; |
| this.pathNodeIdMap[test.path] = node.getId(); |
| node.factoryTest = test; |
| }; |
| |
| /** |
| * Sends an event to Goofy. |
| * @param {string} type the event type (e.g., 'goofy:hello'). |
| * @param {Object} properties of event. |
| */ |
| cros.factory.Goofy.prototype.sendEvent = function(type, properties) { |
| var dict = goog.object.clone(properties); |
| dict.type = type; |
| var serialized = goog.json.serialize(dict); |
| cros.factory.logger.info('Sending event: ' + serialized); |
| this.ws.send(serialized); |
| }; |
| |
| /** |
| * Calls an RPC function and invokes callback with the result. |
| * @param {Object} args |
| * @param {Object=} callback |
| * @param {Object=} opt_errorCallback |
| */ |
| cros.factory.Goofy.prototype.sendRpc = function( |
| method, args, callback, opt_errorCallback) { |
| var request = goog.json.serialize({method: method, params: args, id: 1}); |
| cros.factory.logger.info('RPC request: ' + request); |
| var factoryThis = this; |
| goog.net.XhrIo.send( |
| '/', function() { |
| cros.factory.logger.info('RPC response for ' + method + ': ' + |
| this.getResponseText()); |
| |
| if (this.getLastErrorCode() != goog.net.ErrorCode.NO_ERROR) { |
| factoryThis.logToConsole('RPC error calling ' + method + ': ' + |
| goog.net.ErrorCode.getDebugMessage(this.getLastErrorCode()), |
| 'goofy-internal-error'); |
| // TODO(jsalz): handle error |
| return; |
| } |
| |
| var response = goog.json.unsafeParse(this.getResponseText()); |
| if (response.error) { |
| factoryThis.logToConsole('RPC error calling ' + method + ': ' + |
| goog.debug.expose(response.error), |
| 'goofy-internal-error'); |
| if (opt_errorCallback) { |
| opt_errorCallback.call(factoryThis, response); |
| } |
| return; |
| } |
| |
| if (callback) { |
| callback.call(factoryThis, response.result); |
| } |
| }, |
| 'POST', request); |
| }; |
| |
| /** |
| * Sends a keepalive event if the web socket is open. |
| */ |
| cros.factory.Goofy.prototype.keepAlive = function() { |
| if (this.ws.isOpen()) { |
| this.sendEvent('goofy:keepalive', {'uuid': this.uuid}); |
| } |
| }; |
| |
| cros.factory.Goofy.prototype.LOAD_AVERAGE_FORMAT = ( |
| new goog.i18n.NumberFormat('0.00')); |
| cros.factory.Goofy.prototype.PERCENT_CPU_FORMAT = ( |
| new goog.i18n.NumberFormat('0.0%')); |
| cros.factory.Goofy.prototype.PERCENT_BATTERY_FORMAT = ( |
| new goog.i18n.NumberFormat('0%')); |
| /** |
| * Gets the system status. |
| */ |
| cros.factory.Goofy.prototype.updateStatus = function() { |
| this.sendRpc('get_system_status', [], function(status) { |
| this.systemInfo['ips'] = status['ips']; |
| this.setSystemInfo(this.systemInfo); |
| |
| function setValue(id, value) { |
| var element = document.getElementById(id); |
| goog.dom.classes.enable(element, 'goofy-value-known', |
| value != null); |
| goog.dom.getElementByClass('goofy-value', element |
| ).innerHTML = value; |
| } |
| |
| setValue('goofy-load-average', |
| status['load_avg'] ? |
| this.LOAD_AVERAGE_FORMAT.format(status['load_avg'][0]) : |
| null); |
| |
| if (this.lastStatus) { |
| var lastCpu = goog.math.sum.apply(this, this.lastStatus['cpu']); |
| var currentCpu = goog.math.sum.apply(this, status['cpu']); |
| var lastIdle = this.lastStatus['cpu'][3]; |
| var currentIdle = status['cpu'][3]; |
| var deltaIdle = currentIdle - lastIdle; |
| var deltaTotal = currentCpu - lastCpu; |
| setValue('goofy-percent-cpu', |
| this.PERCENT_CPU_FORMAT.format( |
| (deltaTotal - deltaIdle) / deltaTotal)); |
| } else { |
| setValue('goofy-percent-cpu', null); |
| } |
| |
| var chargeIndicator = document.getElementById( |
| 'goofy-battery-charge-indicator'); |
| var percent = null; |
| var batteryStatus = 'unknown'; |
| if (status.battery) { |
| if (status.battery.fraction_full != null) { |
| percent = this.PERCENT_BATTERY_FORMAT.format( |
| status.battery.fraction_full); |
| } |
| if (goog.array.contains(['Full', 'Charging', 'Discharging', 'Idle'], |
| status.battery.status)) { |
| batteryStatus = status.battery.status.toLowerCase(); |
| } |
| } |
| setValue('goofy-percent-battery', percent); |
| goog.dom.classes.set( |
| chargeIndicator, 'goofy-battery-' + batteryStatus); |
| |
| var temperatures = status['temperatures']; |
| var mainTemperatureIndex = status['main_temperature_index']; |
| var temp = null; |
| // TODO(jsalz): Generalize to select and use the correct |
| // temperature. |
| if (mainTemperatureIndex != null && |
| temperatures && temperatures.length > mainTemperatureIndex && |
| temperatures[mainTemperatureIndex]) { |
| temp = Math.round(temperatures[mainTemperatureIndex]) + '°C'; |
| } |
| setValue('goofy-temperature', temp); |
| |
| var eth_indicator = document.getElementById('goofy-eth-indicator') |
| goog.dom.classes.enable(eth_indicator, "goofy-eth-enabled", |
| status['eth_on']) |
| var wlan_indicator = document.getElementById('goofy-wlan-indicator') |
| goog.dom.classes.enable(wlan_indicator, "goofy-wlan-enabled", |
| status['wlan_on']) |
| |
| this.lastStatus = status; |
| }); |
| }; |
| |
| /** |
| * Writes a message to the console log. |
| * @param {string} message |
| * @param {Object|Array.<string>|string=} opt_attributes attributes to add |
| * to the div element containing the log entry. |
| */ |
| cros.factory.Goofy.prototype.logToConsole = function(message, opt_attributes) { |
| var div = goog.dom.createDom('div', opt_attributes); |
| goog.dom.classes.add(div, 'goofy-log-line'); |
| div.appendChild(document.createTextNode(message)); |
| this.console.appendChild(div); |
| // Scroll to bottom. TODO(jsalz): Scroll only if already at the bottom, |
| // or add scroll lock. |
| var scrollPane = goog.dom.getAncestorByClass(this.console, |
| 'goog-splitpane-second-container'); |
| scrollPane.scrollTop = scrollPane.scrollHeight; |
| }; |
| |
| /** |
| * Logs an "internal" message to the console (as opposed to a line from |
| * console.log). |
| */ |
| cros.factory.Goofy.prototype.logInternal = function(message) { |
| this.logToConsole(message, 'goofy-internal-log'); |
| }; |
| |
| /** |
| * Handles an event sends from the backend. |
| * @param {string} jsonMessage the message as a JSON string. |
| */ |
| cros.factory.Goofy.prototype.handleBackendEvent = function(jsonMessage) { |
| cros.factory.logger.info('Got message: ' + jsonMessage); |
| var message = /** @type Object.<string, Object> */ ( |
| goog.json.unsafeParse(jsonMessage)); |
| |
| if (message.type == 'goofy:hello') { |
| if (this.uuid && message.uuid != this.uuid) { |
| // The goofy process has changed; reload the page. |
| cros.factory.logger.info('Incorrect UUID; reloading'); |
| window.location.reload(); |
| return; |
| } else { |
| this.uuid = message.uuid; |
| // Send a keepAlive to confirm the UUID with the backend. |
| this.keepAlive(); |
| // TODO(jsalz): Process version number information. |
| } |
| } else if (message.type == 'goofy:log') { |
| this.logToConsole(message.message); |
| } else if (message.type == 'goofy:state_change') { |
| this.setTestState(message.path, message.state); |
| } else if (message.type == 'goofy:init_test_ui') { |
| var invocation = this.getOrCreateInvocation( |
| message.test, message.invocation); |
| if (invocation) { |
| goog.dom.iframe.writeContent( |
| invocation.iframe, |
| /** @type {string} */(message['html'])); |
| this.updateCSSClassesInDocument(invocation.iframe.contentDocument); |
| } |
| |
| // In the content window's evaluation context, add our keydown |
| // listener. |
| invocation.iframe.contentWindow.eval( |
| 'window.addEventListener("keydown", ' + |
| 'window.test.invocation.goofy.boundKeyListener)'); |
| } else if (message.type == 'goofy:set_html') { |
| var invocation = this.getOrCreateInvocation( |
| message.test, message.invocation); |
| if (invocation) { |
| if (message.id) { |
| var element = invocation.iframe.contentDocument.getElementById( |
| message.id); |
| if (!message.append && element) { |
| element.innerHTML = ''; |
| } |
| element.innerHTML += message['html']; |
| } else { |
| var body = invocation.iframe.contentDocument.body; |
| if (body) { |
| if (!message.append) { |
| body.innerHTML = ''; |
| } |
| body.innerHTML += message['html']; |
| } else { |
| this.logToConsole( |
| 'Test UI not initialized.', 'goofy-internal-error'); |
| } |
| } |
| } |
| } else if (message.type == 'goofy:run_js') { |
| var invocation = this.getOrCreateInvocation( |
| message.test, message.invocation); |
| if (invocation) { |
| // We need to evaluate the code in the context of the content |
| // window, but we also need to give it a variable. Stash it |
| // in the window and load it directly in the eval command. |
| invocation.iframe.contentWindow.__goofy_args = message['args']; |
| invocation.iframe.contentWindow.eval( |
| 'var args = window.__goofy_args;' + |
| /** @type string */ (message['js'])); |
| delete invocation.iframe.contentWindow.__goofy_args; |
| } |
| } else if (message.type == 'goofy:call_js_function') { |
| var invocation = this.getOrCreateInvocation( |
| message.test, message.invocation); |
| if (invocation) { |
| var func = invocation.iframe.contentWindow.eval(message['name']); |
| if (func) { |
| func.apply(invocation.iframe.contentWindow, message['args']); |
| } else { |
| cros.factory.logger.severe('Unable to find function ' + func + |
| ' in UI for test ' + message.test); |
| } |
| } |
| } else if (message.type == 'goofy:destroy_test') { |
| var invocation = this.invocations[message.invocation]; |
| if (invocation) { |
| invocation.dispose(); |
| } |
| } else if (message.type == 'goofy:system_info') { |
| this.setSystemInfo(message['system_info']); |
| } else if (message.type == 'goofy:pending_shutdown') { |
| this.setPendingShutdown( |
| /** @type {cros.factory.PendingShutdownEvent} */(message)); |
| } |
| }; |
| |
| goog.events.listenOnce(window, goog.events.EventType.LOAD, function() { |
| window.goofy = new cros.factory.Goofy(); |
| window.goofy.init(); |
| }); |