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 }