Command Streaming

loadcmd.htm (hint: right click, save as)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
<!--
  Test/Development/Documentation page; install as plugin to test
-->

<div class="panel panel-primary">
  <div class="panel-heading">Asynchronous Command Streaming</div>
  <div class="panel-body">

    <h3>Synopsis</h3>

    <p class="lead"><code>[jqxhr=] loadcmd(command [,target] [,filter] [,timeout])</code></p>

    <p>The <code>loadcmd()</code> function executes a shell command or evaluates javascript
      code with asynchronous streaming of the output into a target element or onto a function.</p>


    <h3>Basic Usage</h3>

    <p>As this is the underlying function for command buttons, basic usage
      is very similar, but fully scriptable:</p>

    <p>
      <button type="button" class="btn btn-default" onclick="loadcmd('boot status', '#out1')">
        Show boot status
      </button>
    </p>
    <pre id="out1" />

    <p>As with command buttons, you can append the output by prefixing the target
      selector with "+". You can omit the output by passing null as the target. If target
      is a DOM element, loadcmd automatically sets the <code>loading</code> class on the
      target while the command is processed.</p>

    <p>The target element's min-height is automatically fixed to the current height
      of the target before the output is set. This avoids page layout jumps when
      reusing a target for commands.</p>

    <p>If the target is scrollable and the content exceeds the visible area, the
      element is automatically scrolled to show the new content, unless the user has
      done a manual scrolling on the element.</p>

    <p>If the command output stops for <code>timeout</code> seconds, the request is
      aborted and a timeout error is shown. Default timeout is 20 seconds for standard
      commands, 300 seconds for known long running commands.</p>


    <h3>Evaluate Javascript Code</h3>

    <p>To execute Javascript, either pass an object instead of the <code>command</code> string,
      containing the command as property "command" and a second property "type" with
      value "js", or simply use the wrapper call <code>loadjs()</code>. Example:</p>

    <p>
      <button type="button" class="btn btn-default"
        onclick="loadjs('print(OvmsVehicle.Type())', '#out1js')">
        Show vehicle type
      </button>
    </p>
    <pre id="out1js" />

    <p>Javascript evaluation is not limited to a single command or line. Hint: to avoid
      encoding complex JS code in the onclick attribute, store the code in some hidden
      DOM element and read it via <code>$(…).text()</code>.</p>


    <h3>jqXHR Object</h3>

    <p><code>loadcmd()</code> returns the jqXHR (XMLHttpRequest) object in charge for
      the asynchronous execution of the request. This can be used to track the results
      of the execution, check for errors or to abort the command.</p>

    <p>
      <button type="button" class="btn btn-default" id="startcg">Start chargen</button>
      <button type="button" class="btn btn-default" id="abortcg" disabled>Abort chargen</button>
    </p>
    <pre id="out2" style="max-height:200px; overflow:auto;" />

    <p>The jQuery XHR object is also a "thenable", so actions to be performed after
      the command execution can simply be chained to the object. Example:</p>

    <p><button type="button" class="btn btn-default" id="showstat">Show stat</button></p>

    <p>See <a target="_blank" href="http://api.jquery.com/jQuery.ajax/#jqXHR">jQuery
      documentation</a> for full details and options.</p>


    <h3>Filter / Process Output</h3>

    <p>Supply a filter function to hook into the asynchronous output stream. Use filters
      to filter (ahem…) / preprocess / reformat the command output, scan the stream for some
      info you'd like to know as soon as possible, or completely take over the output
      processing.</p>

    <p>The filter function is called when a new chunk of output has arrived or when
      a stream error has occurred. The function gets a message object containing
      the <code>request</code> object and either a <code>text</code> for normal outputs
      or an <code>error</code>, which is a preformatted error output you can use or
      ignore.</p>

    <p>If the filter function returns a string, that will be added to the output target.
      If it returns <code>null</code>, the target will remain untouched.</p>

    <p>Hint: if you just want to scan the text for some info, you can pass on the message
      after your scan to the default <code>standardTextFilter()</code>.</p>

    <p>Example: let's reformat a <code>can status</code> dump into a nice Bootstrap table:</p>

    <p><button type="button" class="btn btn-default" id="canstatus">CAN1 status</button></p>

    <table class="table table-condensed table-border table-striped table-hover" id="canout">
      <thead>
        <tr><th class="col-xs-6">Key</th><th class="col-xs-6">Value</th></tr>
      </thead>
      <tbody/>
    </table>

  </div>
</div>

<script>
(function(){

  /* Show page source: */
  var pagesrc = $('#main').html();
  $('.panel-heading').prepend('<button type="button" class="btn btn-sm btn-info action-showsrc"' +
    ' style="float:right; position:relative; top:-5px;">Show page source</button>');
  $('.action-showsrc').on('click', function() {
    $('<div/>').dialog({
      title: 'Source Code',
      body: '<pre style="font-size:85%; height:calc(100vh - 230px);">'
        + encode_html(pagesrc) + '</pre>',
      size: 'lg',
    });
  });

  /* Example: jqXHR.abort() */
  var xhr;
  $('#startcg').on('click', function() {
    xhr = loadcmd('test chargen 20 600', '#out2');
    $('#abortcg').prop('disabled', false);
  });
  $('#abortcg').on('click', function() {
    xhr.abort();
  });

  /* Example: jqXHR.then() */
  $('#showstat').on('click', function() {
    loadcmd('stat').then(function(output) {
      confirmdialog('STAT result', '<samp>' + output + '</samp>', ['Close']);
    });
  });

  /* Example: filter / output processor */
  $('#canstatus').on('click', function() {
    var $table = $('#canout > tbody');
    var buf = '';
    $table.empty();
    loadcmd('can can1 status', function(msg) {
      if (msg.error) {
        // Render error into table row:
        $('<tr class="danger"><td colspan="2">' + msg.error + '</td></tr>').appendTo($table);
      }
      else if (msg.text) {
        // As this is a stream, the text chunks received need not be single or complete lines.
        // We're interested in lines here, so we buffer the chunks and split the buffer at '\n':
        buf += msg.text;
        var lines = buf.split('\n');
        if (lines.length > 1) {
          buf = lines[lines.length-1];
          for (var i = 0; i < lines.length-1; i++) {
            // Skip empty lines:
            if (lines[i] == "") continue;
            // Split line into columns:
            var col = lines[i].split(/: +/);
            // Create table row & append to table, add some color on error counters:
            if (col[0].match("[Ee]rr"))
              $('<tr class="warning"><th>' + col[0] + '</th><td>' + col[1] + '</td></tr>').appendTo($table);
            else
              $('<tr><th>' + col[0] + '</th><td>' + col[1] + '</td></tr>').appendTo($table);
          }
        }
      }
      // Filter has handled everything:
      return null;
    });
  });

})();
</script>