github.com/hernad/nomad@v1.6.112/ui/app/utils/classes/stream-logger.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  import EmberObject, { computed } from '@ember/object';
     7  import { task } from 'ember-concurrency';
     8  import TextDecoder from 'nomad-ui/utils/classes/text-decoder';
     9  import { decode } from 'nomad-ui/utils/stream-frames';
    10  import AbstractLogger from './abstract-logger';
    11  import { fetchFailure } from './log';
    12  import classic from 'ember-classic-decorator';
    13  
    14  @classic
    15  export default class StreamLogger extends EmberObject.extend(AbstractLogger) {
    16    reader = null;
    17  
    18    static get isSupported() {
    19      return !!window.ReadableStream;
    20    }
    21  
    22    @computed()
    23    get additionalParams() {
    24      return {
    25        follow: true,
    26      };
    27    }
    28  
    29    start() {
    30      return this.poll.perform();
    31    }
    32  
    33    stop() {
    34      const reader = this.reader;
    35      if (reader) {
    36        reader.cancel();
    37      }
    38      return this.poll.cancelAll();
    39    }
    40  
    41    @task(function* () {
    42      const url = this.fullUrl;
    43      const logFetch = this.logFetch;
    44  
    45      const reader = yield logFetch(url).then((res) => {
    46        const reader = res.body.getReader();
    47        // It's possible that the logger was stopped between the time
    48        // polling was started and the log request responded.
    49        // If the logger was stopped, the reader needs to be immediately
    50        // canceled to prevent an endless request running in the background.
    51        if (this.poll.isRunning) {
    52          return reader;
    53        }
    54        reader.cancel();
    55      }, fetchFailure(url));
    56  
    57      if (!reader) {
    58        return;
    59      }
    60  
    61      this.set('reader', reader);
    62  
    63      let streamClosed = false;
    64      let buffer = '';
    65      const decoder = new TextDecoder();
    66  
    67      while (!streamClosed) {
    68        yield reader.read().then(({ value, done }) => {
    69          streamClosed = done;
    70  
    71          // There is no guarantee that value will be a complete JSON object,
    72          // so it needs to be buffered.
    73          buffer += decoder.decode(value, { stream: true });
    74  
    75          // Only when the buffer contains a close bracket can we be sure the buffer
    76          // is in a complete state
    77          if (buffer.indexOf('}') !== -1) {
    78            // The buffer can be one or more complete frames with additional text for the
    79            // next frame
    80            const [, chunk, newBuffer] = buffer.match(/(.*\})(.*)$/);
    81  
    82            // Peel chunk off the front of the buffer (since it represents complete frames)
    83            // and set the buffer to be the remainder
    84            buffer = newBuffer;
    85  
    86            // Assuming the logs endpoint never returns nested JSON (it shouldn't), at this
    87            // point chunk is a series of valid JSON objects with no delimiter.
    88            const { offset, message } = decode(chunk);
    89            if (message) {
    90              this.set('endOffset', offset);
    91              this.write(message);
    92            }
    93          }
    94        });
    95      }
    96    })
    97    poll;
    98  }