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