github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/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 8 export default EmberObject.extend(AbstractLogger, { 9 reader: null, 10 11 additionalParams: computed(() => ({ 12 follow: true, 13 })), 14 15 start() { 16 return this.poll.perform(); 17 }, 18 19 stop() { 20 const reader = this.reader; 21 if (reader) { 22 reader.cancel(); 23 } 24 return this.poll.cancelAll(); 25 }, 26 27 poll: task(function*() { 28 const url = this.fullUrl; 29 const logFetch = this.logFetch; 30 31 const reader = yield logFetch(url).then(res => { 32 const reader = res.body.getReader(); 33 // It's possible that the logger was stopped between the time 34 // polling was started and the log request responded. 35 // If the logger was stopped, the reader needs to be immediately 36 // canceled to prevent an endless request running in the background. 37 if (this.poll.isRunning) { 38 return reader; 39 } 40 reader.cancel(); 41 }, fetchFailure(url)); 42 43 if (!reader) { 44 return; 45 } 46 47 this.set('reader', reader); 48 49 let streamClosed = false; 50 let buffer = ''; 51 const decoder = new TextDecoder(); 52 53 while (!streamClosed) { 54 yield reader.read().then(({ value, done }) => { 55 streamClosed = done; 56 57 // There is no guarantee that value will be a complete JSON object, 58 // so it needs to be buffered. 59 buffer += decoder.decode(value, { stream: true }); 60 61 // Only when the buffer contains a close bracket can we be sure the buffer 62 // is in a complete state 63 if (buffer.indexOf('}') !== -1) { 64 // The buffer can be one or more complete frames with additional text for the 65 // next frame 66 const [, chunk, newBuffer] = buffer.match(/(.*\})(.*)$/); 67 68 // Peel chunk off the front of the buffer (since it represents complete frames) 69 // and set the buffer to be the remainder 70 buffer = newBuffer; 71 72 // Assuming the logs endpoint never returns nested JSON (it shouldn't), at this 73 // point chunk is a series of valid JSON objects with no delimiter. 74 const { offset, message } = decode(chunk); 75 if (message) { 76 this.set('endOffset', offset); 77 this.write(message); 78 } 79 } 80 }); 81 } 82 }), 83 }).reopenClass({ 84 isSupported: !!window.ReadableStream && !isSafari(), 85 }); 86 87 // Fetch streaming doesn't work in Safari yet despite all the primitives being in place. 88 // Bug: https://bugs.webkit.org/show_bug.cgi?id=185924 89 // Until this is fixed, Safari needs to be explicitly targeted for poll-based logging. 90 function isSafari() { 91 const oldSafariTest = /constructor/i.test(window.HTMLElement); 92 const newSafariTest = (function(p) { 93 return p.toString() === '[object SafariRemoteNotification]'; 94 })(!window['safari'] || (typeof window.safari !== 'undefined' && window.safari.pushNotification)); 95 return oldSafariTest || newSafariTest; 96 }