github.com/aitjcize/Overlord@v0.0.0-20240314041920-104a804cf5e8/overlord/app/common/js/FixtureWidget.jsx (about)

     1  // Copyright 2015 The Chromium OS Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  //
     5  // External dependencies:
     6  // - JsRender: https://github.com/BorisMoore/jsrender
     7  //
     8  // Internal dependencies:
     9  // - UploadProgressWidget
    10  //
    11  // View for FixtureWidget:
    12  // - FixtureWidget
    13  //   props:
    14  //     app: react reference to the app object with addTerminal method
    15  //     client: a overlord client client object with properties object
    16  //     progressBars: an react reference to UploadProgressWidget instance
    17  //  - Display
    18  //  - Lights
    19  //  - Terminals
    20  //  - Controls
    21  //  - MainLog
    22  //  - AuxLogs
    23  //   - AuxLog
    24  
    25  var LOG_BUF_SIZE = 8192;
    26  
    27  var FIXTURE_WINDOW_WIDTH = 420;
    28  var FIXTURE_WINDOW_MARGIN = 10;
    29  
    30  var LIGHT_CSS_MAP = {
    31    'light-toggle-off': 'label-danger',
    32    'light-toggle-on': 'label-success'
    33  };
    34  
    35  // FixtureWidget defines the layout and behavior of a fixture window,
    36  // which has display, lights, terminals, controls and logs.
    37  //
    38  // Usage:
    39  // A terminal description object that would be passed to app.addTerminal would
    40  // looks like the following in json:
    41  // {
    42  //   "name": "NUC",
    43  //   "mid": "ghost 1",
    44  //   // @path attribute is optional, without @path, it means that we are
    45  //   // connecting to the fixture itself.
    46  //   "path": "some path"
    47  // }
    48  //
    49  // A client object would looks like the following in json:
    50  // {
    51  //   "mid": "machine ID",
    52  //   "sid": "serial ID",
    53  //   // see properties.sample.json
    54  //   "properties": {
    55  //     "ip": "127.0.0.1",
    56  //     "ui": {
    57  //       // The main command which updates ui states.
    58  //       // "update_ui_status" is a script we wrote that will respect
    59  //       // @init_cmd and @poll attributes in lights and display.data, you can
    60  //       // implement your own script instead.
    61  //       "update_ui_command": "update_ui_status",
    62  //       // Lights are used to show current status of the fixture, lights has
    63  //       // two states: on and off, which is represent by setting "light"
    64  //       // attribute to 'light-toggle-on' or 'light-toggle-off' (see below)
    65  //       "lights": [
    66  //         {
    67  //           // Identifier of this light, if the output of @command contains
    68  //           // LIGHT[@id]='light-toggle-on', then @light will be set to on.
    69  //           "id": "ccd",
    70  //           // Text to be shown
    71  //           "label": "CCD",
    72  //           // Set default state to off
    73  //           "light": "light-toggle-off",
    74  //           // Command to execute when clicked
    75  //           "command": "case_close_debug",
    76  //           // Will be called when the FixtureWidget is opened.
    77  //           "init_cmd": "case_close_debug status"
    78  //         },
    79  //         {
    80  //           "id": "dut-lid",
    81  //           "label": "DUT LID",
    82  //           "light": "light-toggle-off",
    83  //           // @cmd will be execute every @interval milliseconds, you can
    84  //           // output LIGHT[@id]='light-toggle-on' to change the light.
    85  //           "poll": {
    86  //             "cmd": "check_dut_exists -t lid",
    87  //             "interval": 20000
    88  //           }
    89  //         }, ...
    90  //       ],
    91  //       // A list of terminals connected to this fixture, for example, there
    92  //       // might be a terminal for fixture itself and a terminal for DUT.
    93  //       "terminals": [
    94  //         // Without @path_cmd attribute, will connect to fixture itself.
    95  //         {
    96  //           "name": "NUC"
    97  //         },
    98  //         // @path_cmd will be used to get the path of device.
    99  //         {
   100  //           "name": "AP",
   101  //           "path_cmd": "ls /dev/google/Ryu_debug-*/serial/AP 2>/dev/null",
   102  //         }
   103  //       ],
   104  //       // A display section
   105  //       "display": {
   106  //         // A jsrender template
   107  //         "template": "<b>Report</b><ul><li>Version: {{:version}}</li>"
   108  //                     "<li>Status: {{:status}}</li></ul>",
   109  //         "data": [
   110  //           {
   111  //             // id: the name of data binding in the template
   112  //             "id": "version",
   113  //             // Will be called when the FixtureWidget is opened.
   114  //             "init_cmd": "get_version"
   115  //           },
   116  //           {
   117  //             "id": "status",
   118  //             // @cmd will be execute every @interval milliseconds, you can
   119  //             // output DATA[@id]='value' to change the binding value.
   120  //             "poll": {
   121  //               "cmd": "get_status",
   122  //               "interval": 20000
   123  //             }
   124  //           }, ...
   125  //         ]
   126  //       },
   127  //       // A list of buttons to control some functionality of the fixture.
   128  //       "controls": [
   129  //         // A command
   130  //         {
   131  //           "name": "Factory Restart",
   132  //           "command": "factory_restart"
   133  //         },
   134  //         // A command that will be toggled between two state.
   135  //         {
   136  //           "name": "Voltage Measurement",
   137  //           "type": "toggle",
   138  //           "on_command": "command to start measuring voltage",
   139  //           "off_command": "command to stop measuring"
   140  //         },
   141  //         // A button that allow uploading a file, then execute a command
   142  //         {
   143  //           "name": "Update Toolkit",
   144  //           "type": "upload",
   145  //           "dest": "/tmp/install_factory_toolkit.run",
   146  //           // @command is optional, you can omit this if you don't need to
   147  //           // execute any command.
   148  //           "command": "rm -rf /usr/local/factory && "
   149  //                      "sh /tmp/install_factory_toolkit.run -- -y &&"
   150  //                      "factory_restart"
   151  //         },
   152  //         // A button that allow execute a command, then download a file
   153  //         {
   154  //           "name": "Download Log",
   155  //           "type": "download",
   156  //           // @command is optional, you can omit this if you don't need to
   157  //           // execute any command.
   158  //           "command": "dmesg > /tmp/dmesg.log",
   159  //
   160  //           // The filename can be specified in both static @filename
   161  //           // attribute, or a @filename_cmd attribute, which the output
   162  //           // of the command is the download filename.
   163  //           "filename": "/tmp/dmesg.log",
   164  //           // or (exclusively)
   165  //           "filename_cmd": "get_filename_cmd"
   166  //         },
   167  //         // A button that opens a link
   168  //         {
   169  //           "name": "VNC",
   170  //           "type": "link",
   171  //           // @url is a URL template, here is a list of supported attributes:
   172  //           // host: the hostname of the webserver serving this page
   173  //           // port: the HTTP port of the webserver serving this page
   174  //           // client: the client object
   175  //           "url": "/third_party/noVNC/vnc_auto.html?host={{:host}}&"
   176  //                  "port={{:port}}&path=api/agent/forward/{{:client.mid}}"
   177  //                  "%3fport=5901"
   178  //         },
   179  //         // A group of commands
   180  //         {
   181  //           "name": "Fixture control"
   182  //           "group": [
   183  //             {
   184  //               "name": "whale close",
   185  //               "command": "whale close"
   186  //             },
   187  //             {
   188  //               "name": "whale open",
   189  //               "command": "whale open"
   190  //             },
   191  //             {
   192  //               "name": "io insertion",
   193  //               "command": "whale insert"
   194  //             },
   195  //             {
   196  //               "name": "charging",
   197  //               "command": "whale charge"
   198  //             }
   199  //           ]
   200  //         }
   201  //       ],
   202  //       // Path to the log files, FixtureWidget will keep polling the latest
   203  //       // content of these file.
   204  //       "logs": [
   205  //         "/var/log/factory.log", ...
   206  //       ]
   207  //     },
   208  //     // What catagories this fixture belongs to. If it contains "ui", an "UI"
   209  //     // button will be shown on the /dashboard page. If it contains "whale",
   210  //     // it will be shown on the /whale page.
   211  //     "context": [
   212  //       "ui", "whale", ...
   213  //     ]
   214  //   }
   215  // }
   216  var FixtureWidget = React.createClass({
   217    executeRemoteCmd: function (mid, cmd) {
   218      if (!this.isMounted()) {
   219        return;
   220      }
   221      var url = "ws" + ((window.location.protocol == "https:")? "s": "" ) +
   222                "://" + window.location.host + "/api/agent/shell/" + mid +
   223                "?command=" + encodeURIComponent(cmd);
   224      var sock = new WebSocket(url);
   225      var deferred = $.Deferred();
   226  
   227      sock.onopen = function (event) {
   228        sock.onmessage = function (msg) {
   229          if (!this.isMounted()) {
   230            sock.close();
   231            return;
   232          }
   233          if (msg.data instanceof Blob) {
   234            ReadBlobAsText(msg.data, function(text) {
   235              this.refs.mainlog.appendLog(text);
   236            }.bind(this));
   237          }
   238        }.bind(this)
   239      }.bind(this)
   240  
   241      sock.onclose = function (event) {
   242        deferred.resolve();
   243      }
   244      this.socks.push(sock);
   245  
   246      return deferred.promise();
   247    },
   248    extractUIMessages: function (text) {
   249      if (typeof(this.refs.lights) != "undefined") {
   250        text = this.refs.lights.extractLightMessages(text);
   251      }
   252      if (typeof(this.refs.display) != "undefined") {
   253        text = this.refs.display.extractDataMessages(text);
   254      }
   255      return text;
   256    },
   257    componentDidMount: function () {
   258      var client = this.props.client;
   259      var update_ui_command =
   260          client.properties.ui.update_ui_command || 'update_ui_status';
   261      setTimeout(function() {
   262        this.executeRemoteCmd(client.mid, update_ui_command);
   263      }.bind(this), 1000);
   264    },
   265    componentWillUnmount: function () {
   266      for (var i = 0; i < this.socks.length; ++i) {
   267        this.socks[i].close();
   268      }
   269    },
   270    getInitialState: function () {
   271      this.socks = [];
   272      return {};
   273    },
   274    render: function () {
   275      var client = this.props.client;
   276      var ui = client.properties.ui;
   277      var style = {
   278        width: FIXTURE_WINDOW_WIDTH + 'px',
   279        margin: FIXTURE_WINDOW_MARGIN + 'px',
   280      };
   281      var display = ui.display && (
   282            <Display ref="display" client={client} fixture={this} />
   283          ) || "";
   284      var lights = ui.lights && ui.lights.length > 0 && (
   285            <Lights ref="lights" client={client} fixture={this} />
   286          ) || "";
   287      var terminals = ui.terminals && ui.terminals.length > 0 && (
   288            <Terminals client={client} app={this.props.app} fixture={this} />
   289          ) || "";
   290      var controls = ui.controls && ui.controls.length > 0 && (
   291            <Controls ref="controls" client={client} fixture={this}
   292             progressBars={this.props.progressBars} />
   293          ) || "";
   294      var auxlogs = ui.logs && ui.logs.length > 0 && (
   295            <AuxLogs client={client} fixture={this} />
   296          ) || "";
   297      return (
   298        <div className="fixture-block panel panel-success" style={style}>
   299          <div className="panel-heading text-center">{abbr(client.mid, 60)}</div>
   300          <div className="panel-body">
   301            {display}
   302            {lights}
   303            {terminals}
   304            {controls}
   305            <MainLog ref="mainlog" fixture={this} id={client.mid} />
   306            {auxlogs}
   307          </div>
   308        </div>
   309      );
   310    }
   311  });
   312  
   313  var Display = React.createClass({
   314    updateDisplay: function (key, value) {
   315      this.setState(function (state, props) {
   316        state[key] = value;
   317      });
   318    },
   319    extractDataMessages: function (msg) {
   320      var patt = /DATA\[(.*?)\]\s*=\s*'(.*?)'\n?/g;
   321      var found;
   322      while (found = patt.exec(msg)) {
   323        this.updateDisplay(found[1], found[2]);
   324      }
   325      return msg.replace(patt, "");
   326    },
   327    getInitialState: function () {
   328      var display = this.props.client.properties.ui.display;
   329      var data = {};
   330      for (var i = 0; i < display.data.length; i++) {
   331        data[display.data[i].id] = ""
   332      }
   333      return data;
   334    },
   335    componentWillMount: function() {
   336      var display = this.props.client.properties.ui.display;
   337      this.template = $.templates(display.template);
   338    },
   339    render: function () {
   340      var client = this.props.client;
   341      var displayHTML = this.template.render(this.state);
   342      return (
   343        <div className="status-block well well-sm">
   344          <div dangerouslySetInnerHTML={{__html: displayHTML}} />
   345        </div>
   346      );
   347    }
   348  });
   349  
   350  var Lights = React.createClass({
   351    updateLightStatus: function (id, status_class) {
   352      var node = $(this.refs[id]);
   353      node.removeClass(this.refs[id].props.prevLight);
   354      node.addClass(status_class);
   355      this.refs[id].props.prevLight = status_class;
   356    },
   357    extractLightMessages: function (msg) {
   358      var patt = /LIGHT\[(.*?)\]\s*=\s*'(.*?)'\n?/g;
   359      var found;
   360      while (found = patt.exec(msg)) {
   361        this.updateLightStatus(found[1], LIGHT_CSS_MAP[found[2]]);
   362      }
   363      return msg.replace(patt, "");
   364    },
   365    render: function () {
   366      var client = this.props.client;
   367      var lights = client.properties.ui.lights || [];
   368      return (
   369        <div className="status-block well well-sm">
   370        {
   371          lights.map(function (light) {
   372            var extra_css = "";
   373            var extra = {};
   374            if (typeof(light.command) != "undefined") {
   375              extra_css = "status-light-clickable";
   376              extra.onClick = function() {
   377                this.props.fixture.executeRemoteCmd(client.mid, light.command);
   378              }.bind(this);
   379            }
   380            var light_css = LIGHT_CSS_MAP[light.light];
   381            return (
   382              <span key={light.id} className={"label " + extra_css + " " +
   383                light_css} prevLight={light_css} ref={light.id} {...extra}>
   384                {light.label}
   385              </span>
   386            );
   387          }.bind(this))
   388        }
   389        </div>
   390      );
   391    }
   392  });
   393  
   394  var Terminals = React.createClass({
   395    onTerminalClick: function (event) {
   396      var target = $(event.target);
   397      var mid = target.data("mid");
   398      var term = target.data("term");
   399      var id = mid + "::" + term.name;
   400  
   401      // Add mid reference to term object
   402      term.mid = mid;
   403  
   404      if (typeof(term.path_cmd) != "undefined" &&
   405          term.path_cmd.match(/^\s+$/) == null) {
   406        getRemoteCmdOutput(mid, term.path_cmd).done(function (path) {
   407          if (path.replace(/^\s+|\s+$/g, "") == "") {
   408            alert("This TTY device does not exist!");
   409          } else {
   410            term.path = path;
   411            this.props.app.addTerminal(id, term);
   412          }
   413        }.bind(this));
   414        return;
   415      }
   416  
   417      this.props.app.addTerminal(id, term);
   418    },
   419    render: function () {
   420      var client = this.props.client;
   421      var terminals = client.properties.ui.terminals || [];
   422      return (
   423        <div className="status-block well well-sm">
   424        {
   425          terminals.map(function (term) {
   426            return (
   427              <button className="btn btn-xs btn-info" data-mid={client.mid}
   428                  data-term={JSON.stringify(term)} key={term.name}
   429                  onClick={this.onTerminalClick}>
   430              {term.name}
   431              </button>
   432            );
   433          }.bind(this))
   434        }
   435        </div>
   436      );
   437    }
   438  });
   439  
   440  var Controls = React.createClass({
   441    onCommandClicked: function (event) {
   442      var target = $(event.target);
   443      var ctrl = target.data("ctrl");
   444      var mid = target.data("mid");
   445      var fixture = this.props.fixture;
   446  
   447      if (ctrl.type == "toggle") {
   448        if (target.hasClass("active")) {
   449          fixture.executeRemoteCmd(mid, ctrl.off_command);
   450          target.removeClass("active");
   451        } else {
   452          fixture.executeRemoteCmd(mid, ctrl.on_command);
   453          target.addClass("active");
   454        }
   455      } else if (ctrl.type == "download") {
   456        // Helper function for downloading a file
   457        var downloadFile = function (filename) {
   458          var url = window.location.protocol + "//" + window.location.host +
   459                    "/api/agent/download/" + mid +
   460                    "?filename=" + filename;
   461          $("<iframe src='" + url + "' style='display:none'>" +
   462            "</iframe>").appendTo('body');
   463          target.text(ctrl.name);
   464          target.removeClass("active");
   465        }
   466        var startDownload = function (file_path) {
   467          // Check if there is filename_cmd
   468          if (typeof(ctrl.filename_cmd) != "undefined") {
   469            getRemoteCmdOutput(mid, ctrl.filename_cmd)
   470              .done(function (path) { downloadFile(path); });
   471          } else {
   472            downloadFile(file_path);
   473          }
   474        }
   475        var file_path = ctrl.filename;
   476        if (typeof(file_path) == "undefined" &&
   477            typeof(ctrl.filename_cmd) == "undefined") {
   478          file_path = prompt('Enter abosolute path to file:');
   479        }
   480        if ((file_path == null || file_path == "") &&
   481            typeof(ctrl.filename_cmd) == "undefined") {
   482          return;
   483        }
   484  
   485        target.text(ctrl.name + " (Processing...)");
   486        target.addClass("active");
   487        if (typeof(ctrl.command) != "undefined") {
   488          fixture.executeRemoteCmd(mid, ctrl.command)
   489            .done(function() { startDownload(file_path); });
   490        } else {
   491          startDownload(file_path);
   492        }
   493      } else {
   494        fixture.executeRemoteCmd(mid, ctrl.command);
   495      }
   496    },
   497    onUploadButtonChanged: function (event) {
   498      var file = event.target;
   499      var mid = $(file).data("mid");
   500      var ctrl = $(file).data("ctrl");
   501  
   502      var runCommand = function () {
   503        if (typeof(ctrl.command) != "undefined") {
   504          this.props.fixture.executeRemoteCmd(mid, ctrl.command);
   505        }
   506        // Reset the file value, so user can click the button again.
   507        file.value = "";
   508      };
   509  
   510      if (file.value != "") {
   511        this.props.progressBars.upload("/api/agent/upload/" + mid,
   512                                       file.files[0], ctrl.dest,
   513                                       undefined, runCommand.bind(this));
   514      }
   515    },
   516    componentDidMount: function () {
   517      $('input[type=file]').fileinput();
   518    },
   519    render: function () {
   520      var client = this.props.client;
   521      var mid = client.mid;
   522      var controls = client.properties.ui.controls || [];
   523      var btnClasses = "btn btn-xs btn-primary";
   524      return (
   525        <div className="controls-block well well-sm">
   526        {
   527          controls.map(function (control) {
   528            if (typeof(control.group) != "undefined") { // sub-group
   529              return (
   530                <div className="well well-sm well-inner" key={control.name}>
   531                {control.name}<br />
   532                {
   533                  control.group.map(function (ctrl) {
   534                    return (
   535                      <button key={ctrl.name}
   536                          className="command-btn btn btn-xs btn-warning"
   537                          data-mid={mid} data-ctrl={JSON.stringify(ctrl)}
   538                          onClick={this.onCommandClicked}>
   539                        {ctrl.name}
   540                      </button>
   541                    );
   542                  }.bind(this))
   543                }
   544                </div>
   545              );
   546            }
   547            if (control.type == "upload") {
   548              return (
   549                <input type="file" className="file"
   550                 data-browse-label={control.name}
   551                 data-browse-icon="" data-show-preview="false"
   552                 data-show-caption="false" data-show-upload="false"
   553                 data-show-remove="false" data-browse-class={btnClasses}
   554                 data-mid={mid} data-ctrl={JSON.stringify(control)}
   555                 onChange={this.onUploadButtonChanged} />
   556              );
   557            } else if (control.type == "link") {
   558              var data = {
   559                'host': location.hostname,
   560                'port': location.port,
   561                'client': client
   562              };
   563              var url = $.templates(control.url).render(data);
   564              return (
   565                <a key={control.name} className={"command-btn " + btnClasses}
   566                 href={url} target="_blank">
   567                  {control.name}
   568                </a>
   569              );
   570            } else {
   571              return (
   572                <div key={control.name}
   573                 className={"command-btn " + btnClasses}
   574                 data-mid={mid} data-ctrl={JSON.stringify(control)}
   575                 onClick={this.onCommandClicked}>
   576                  {control.name}
   577                </div>
   578              );
   579            }
   580          }.bind(this))
   581        }
   582        </div>
   583      );
   584    }
   585  });
   586  
   587  var MainLog = React.createClass({
   588    appendLog: function (text) {
   589      var odiv = this.odiv;
   590      var innerText = $(odiv).text();
   591  
   592      text = this.props.fixture.extractUIMessages(text);
   593      innerText += text;
   594      if (innerText.length > LOG_BUF_SIZE) {
   595        innerText = innerText.substr(innerText.length -
   596                                     LOG_BUF_SIZE, LOG_BUF_SIZE);
   597      }
   598      $(odiv).text(innerText);
   599      odiv.scrollTop = odiv.scrollHeight;
   600    },
   601    componentDidMount: function () {
   602      this.odiv = this.refs["log-" + this.props.id];
   603    },
   604    render: function () {
   605      return (
   606        <div className="log log-main well well-sm" ref={"log-" + this.props.id}>
   607        </div>
   608      );
   609    }
   610  });
   611  
   612  var AuxLogs = React.createClass({
   613    render: function () {
   614      var client = this.props.client;
   615      var logs = client.properties.ui.logs || [];
   616      return (
   617        <div className="log-block">
   618          {
   619            logs.map(function (filename) {
   620              return (
   621                <AuxLog key={filename} mid={client.mid} filename={filename}
   622                 fixture={this.props.fixture}/>
   623              )
   624            }.bind(this))
   625          }
   626        </div>
   627      );
   628    }
   629  });
   630  
   631  var AuxLog = React.createClass({
   632    componentDidMount: function () {
   633      var url = "ws" + ((window.location.protocol == "https:")? "s": "" ) +
   634                "://" + window.location.host + "/api/agent/shell/" +
   635                this.props.mid + "?command=" +
   636                encodeURIComponent("tail -f " + this.props.filename);
   637      var sock = new WebSocket(url);
   638  
   639      sock.onopen = function () {
   640        var odiv = this.refs["log-" + this.props.mid];
   641        sock.onmessage = function (msg) {
   642          if (msg.data instanceof Blob) {
   643            ReadBlobAsText(msg.data, function (text) {
   644              var innerText = $(odiv).text();
   645              text = this.props.fixture.extractUIMessages(text);
   646              innerText += text;
   647              if (innerText.length > LOG_BUF_SIZE) {
   648                innerText = innerText.substr(innerText.length -
   649                                             LOG_BUF_SIZE, LOG_BUF_SIZE);
   650              }
   651              $(odiv).text(innerText);
   652              odiv.scrollTop = odiv.scrollHeight;
   653            }.bind(this));
   654          }
   655        }.bind(this)
   656      }.bind(this)
   657      this.sock = sock;
   658    },
   659    componentWillUnmount: function() {
   660      this.sock.close();
   661    },
   662    render: function () {
   663      return (
   664        <div>
   665          {this.props.filename + ":"}
   666          <div className="log log-aux well well-sm" ref={"log-" + this.props.mid}>
   667          </div>
   668        </div>
   669      );
   670    }
   671  });