diff --git a/src/helpers/APIHelper.ts b/src/helpers/APIHelper.ts
index 9ad09d9..b1a4972 100644
--- a/src/helpers/APIHelper.ts
+++ b/src/helpers/APIHelper.ts
@@ -147,9 +147,9 @@ export class APIHelper {
ctx.body = `
GET /api/status
GET /api/list
-POST /api/playURL/:windowId body: {"url": "", "jsCode": "" }
-POST /api/restart/:windowId
-POST /api/stop/:windowId
+PUT /api/playURL/:windowId body: {"url": "", "jsCode": "" }
+PUT /api/restart/:windowId
+PUT /api/stop/:windowId
POST /api/execute/:windowId body: {"jsCode": "" }
`
})
diff --git a/src/helpers/WindowHelper.ts b/src/helpers/WindowHelper.ts
index c3eca8e..df3ac39 100644
--- a/src/helpers/WindowHelper.ts
+++ b/src/helpers/WindowHelper.ts
@@ -6,6 +6,7 @@ import { Logger } from '../lib/logging'
import { ReportStatusIpcPayload, StatusCode, StatusObject } from '../lib/api'
import * as path from 'path'
import urlJoin = require('url-join')
+import { Queue } from '../lib/queue'
export class WindowHelper extends EventEmitter {
private window: BrowserWindow
@@ -27,6 +28,8 @@ export class WindowHelper extends EventEmitter {
*/
private updateHash = 0
+ private queue = new Queue()
+
constructor(
private logger: Logger,
public readonly id: string,
@@ -36,6 +39,14 @@ export class WindowHelper extends EventEmitter {
) {
super()
+ // Put the public methods in a queue, to ensure that they are run in order:
+ this.close = this.queue.bindMethod(this.close.bind(this), { reason: 'close' })
+ this.updateConfig = this.queue.bindMethod(this.updateConfig.bind(this), { reason: 'updateConfig' })
+ this.playURL = this.queue.bindMethod(this.playURL.bind(this), { reason: 'playURL' })
+ this.restart = this.queue.bindMethod(this.restart.bind(this))
+ this.stop = this.queue.bindMethod(this.stop.bind(this), { reason: 'stop' })
+ this.executeJavascript = this.queue.bindMethod(this.executeJavascript.bind(this))
+
this.window = new BrowserWindow({
height: this.config.height,
width: this.config.width,
@@ -53,19 +64,19 @@ export class WindowHelper extends EventEmitter {
this.window.on('resized', () => {
this.logger.info(`Window "${this.id}": resized`)
- this.updateSizeAndPosition()
+ this._updateSizeAndPosition()
})
this.window.on('moved', () => {
this.logger.info(`Window "${this.id}": moved`)
- this.updateSizeAndPosition()
+ this._updateSizeAndPosition()
})
this.window.on('maximize', () => {
this.logger.info(`Window "${this.id}": maximized`)
- this.updateSizeAndPosition()
+ this._updateSizeAndPosition()
})
this.window.on('unmaximize', () => {
this.logger.info(`Window "${this.id}": unmaximized`)
- this.updateSizeAndPosition()
+ this._updateSizeAndPosition()
})
this.window.on('focus', () => {
this.emit('focus')
@@ -91,106 +102,105 @@ export class WindowHelper extends EventEmitter {
public get url(): string | null {
return this._url
}
- // public get status(): StatusObject {
- // return this._status
- // }
public async init(): Promise {
// Set the user-agent:
this.userAgent = this.window.webContents.getUserAgent() + ' sofie-chef'
- await this.updateWindow()
+ await this._updateWindow()
// Trigger loading default page:
// await this.restart()
// await mainWindow.loadFile(path.join(__dirname, '../static/index.html'))
// await mainWindow.loadURL(`file://${app.getAppPath()}/dist/index.html`)
}
+ /** Closes the window. */
public async close(): Promise {
+ // Note: This Method runs in a queue!
this.logger.info(`Closing window "${this.id}"`)
this.window.close()
}
public async updateConfig(sharedConfig: ConfigWindowShared, config: ConfigWindow): Promise {
+ // Note: This Method runs in a queue!
const oldConfig = this._config
this._sharedConfig = sharedConfig
this._config = config
if (!_.isEqual(oldConfig, config)) {
- await this.updateWindow(oldConfig)
+ await this._updateWindow(oldConfig)
}
}
- public async updateWindow(oldConfig?: ConfigWindow): Promise {
- if (
- this.config.x !== undefined &&
- this.config.y !== undefined &&
- this.config.height !== undefined &&
- this.config.width !== undefined
- ) {
- // Hack to make it work on Windows with multi-dpi screens
- // Ref: https://github.com/electron/electron/pull/10972
- const bestDisplay = screen.getDisplayMatching({
- height: this.config.height,
- width: this.config.width,
- x: this.config.x,
- y: this.config.y,
- })
-
- const windowBounds = {
- x: Math.max(this.config.x, bestDisplay.workArea.x),
- y: Math.max(this.config.y, bestDisplay.workArea.y),
- width: Math.min(this.config.width, bestDisplay.workArea.width),
- height: Math.min(this.config.height, bestDisplay.workArea.height),
- }
+ public toggleFullScreen(): void {
+ this.window.setFullScreen(!this.window.isFullScreen())
+ this._updateSizeAndPosition()
+ }
+ public toggleDevTools(): void {
+ this.window.webContents.toggleDevTools()
+ }
+ /** Restarts (reloads) the window. */
+ public async restart(): Promise {
+ // Note: This Method runs in a queue!
+ return this._restart()
+ }
+ /** Play the specified URL in the window */
+ public async playURL(url: string | null): Promise {
+ // Note: This Method runs in a queue!
+ return this._playURL(url)
+ }
+ /**
+ * Stops playing the content in window
+ */
+ public async stop(): Promise {
+ // Note: This Method runs in a queue!
+ await this._playURL(null)
+ }
+ /**
+ * Executes a javascript inside the web player
+ * (This Method runs in a queue.)
+ */
+ public async executeJavascript(script: string): Promise {
+ await this.window.webContents.executeJavaScript(script)
+ }
+ public getURL(): string {
+ const windowUrl = this._url ?? this._config.defaultURL
- this.window.setBounds(windowBounds)
+ if (this._sharedConfig.baseURL && !windowUrl.match(/^(?:[a-z+]+:)?\/\//i)) {
+ // URL does not look absolute, add the baseURL
+ return urlJoin(this._sharedConfig.baseURL, windowUrl)
} else {
- this.window.setBounds({
- width: this.config.width,
- height: this.config.height,
- })
+ return windowUrl
}
+ }
+ public receiveExternalStatus(browserWindow: BrowserWindow, payload: ReportStatusIpcPayload): void {
+ if (browserWindow !== this.window) return
- this.window.setFullScreen(this.config.fullScreen)
-
- if (this.config.onTop) {
- // Note: Some popups are only displayed below if running in fullscreen as well.
- this.window.setAlwaysOnTop(true, 'screen-saver')
- } else {
- this.window.setAlwaysOnTop(false)
+ this.status = {
+ statusCode: payload.status,
+ message: payload.message || '',
}
+ }
- if (this.config.zoomFactor !== oldConfig?.zoomFactor) {
- this.window.webContents.setZoomFactor((this.config.zoomFactor ?? 100) / 100)
- }
- if (
- this.window.webContents.getURL() !== this.getURL() ||
- this.config.displayDebug !== oldConfig?.displayDebug ||
- this.config.hideCursor !== oldConfig?.hideCursor ||
- this.config.hideScrollbar !== oldConfig?.hideScrollbar
- ) {
- await this.restart()
+ public get status(): StatusObject {
+ let status = this._status
+ if (this._contentStatus && this._contentStatus.statusCode > status.statusCode) {
+ status = this._contentStatus
}
-
- this.window.moveTop()
- }
- public hasFocus(): boolean {
- return this.window.isFocused()
- }
- public toggleFullScreen(): void {
- this.window.setFullScreen(!this.window.isFullScreen())
- this.updateSizeAndPosition()
+ return status
}
- public toggleDevTools(): void {
- this.window.webContents.toggleDevTools()
+ private set status(status: StatusObject) {
+ if (this._status.statusCode !== status.statusCode || this._status.message !== status.message) {
+ this._status = status
+ this._emitStatus()
+ }
}
- /** Play the specified URL in the window */
- async playURL(url: string | null): Promise {
+
+ private async _playURL(url: string | null): Promise {
if (this._url !== url) {
this._url = url
- await this.restart()
+ await this._restart()
}
}
/** Restarts (reloads) the window */
- async restart(): Promise {
+ private async _restart(): Promise {
delete this._contentStatus
const updateHash = ++this.updateHash
@@ -210,18 +220,18 @@ export class WindowHelper extends EventEmitter {
await this.window.webContents.insertCSS(`html, body { background-color: ${defaultColor}; }`)
}
- this.setupWebContentListeners()
+ this._setupWebContentListeners()
this.window.setTitle(this.title)
this.window.webContents.setZoomFactor((this.config.zoomFactor ?? 100) / 100)
if (this.config.displayDebug) {
- await this.displayDebugOverlay()
+ await this._displayDebugOverlay()
}
if (updateHash !== this.updateHash) return // Abort if the updateHash has changed
- await this.injectUpdateCSS()
+ await this._injectUpdateCSS()
} catch (err) {
this.status = {
statusCode: StatusCode.ERROR,
@@ -235,42 +245,66 @@ export class WindowHelper extends EventEmitter {
message: ``,
}
}
- /** Stops playing the content in window */
- async stop(): Promise {
- await this.playURL(null)
- }
- /** Executes a javascript inside the web player */
- async executeJavascript(script: string): Promise {
- await this.window.webContents.executeJavaScript(script)
+
+ private _emitStatus() {
+ this.emit('status', this.status)
}
+ private async _updateWindow(oldConfig?: ConfigWindow): Promise {
+ if (
+ this.config.x !== undefined &&
+ this.config.y !== undefined &&
+ this.config.height !== undefined &&
+ this.config.width !== undefined
+ ) {
+ // Hack to make it work on Windows with multi-dpi screens
+ // Ref: https://github.com/electron/electron/pull/10972
+ const bestDisplay = screen.getDisplayMatching({
+ height: this.config.height,
+ width: this.config.width,
+ x: this.config.x,
+ y: this.config.y,
+ })
- receiveExternalStatus(browserWindow: BrowserWindow, payload: ReportStatusIpcPayload): void {
- if (browserWindow !== this.window) return
+ const windowBounds = {
+ x: Math.max(this.config.x, bestDisplay.workArea.x),
+ y: Math.max(this.config.y, bestDisplay.workArea.y),
+ width: Math.min(this.config.width, bestDisplay.workArea.width),
+ height: Math.min(this.config.height, bestDisplay.workArea.height),
+ }
- this.status = {
- statusCode: payload.status,
- message: payload.message || '',
+ this.window.setBounds(windowBounds)
+ } else {
+ this.window.setBounds({
+ width: this.config.width,
+ height: this.config.height,
+ })
}
- }
- public get status(): StatusObject {
- let status = this._status
- if (this._contentStatus && this._contentStatus.statusCode > status.statusCode) {
- status = this._contentStatus
+ this.window.setFullScreen(this.config.fullScreen)
+
+ if (this.config.onTop) {
+ // Note: Some popups are only displayed below if running in fullscreen as well.
+ this.window.setAlwaysOnTop(true, 'screen-saver')
+ } else {
+ this.window.setAlwaysOnTop(false)
}
- return status
- }
- private set status(status: StatusObject) {
- if (this._status.statusCode !== status.statusCode || this._status.message !== status.message) {
- this._status = status
- this.emitStatus()
+
+ if (this.config.zoomFactor !== oldConfig?.zoomFactor) {
+ this.window.webContents.setZoomFactor((this.config.zoomFactor ?? 100) / 100)
}
- }
- private emitStatus() {
- this.emit('status', this.status)
+ if (
+ this.window.webContents.getURL() !== this.getURL() ||
+ this.config.displayDebug !== oldConfig?.displayDebug ||
+ this.config.hideCursor !== oldConfig?.hideCursor ||
+ this.config.hideScrollbar !== oldConfig?.hideScrollbar
+ ) {
+ await this._restart()
+ }
+
+ this.window.moveTop()
}
- private updateSizeAndPosition() {
+ private _updateSizeAndPosition() {
this.config.fullScreen = this.window.isFullScreen()
// No need to update position and size if it's fullscreen anyway.
@@ -285,17 +319,15 @@ export class WindowHelper extends EventEmitter {
this.emit('window-has-been-modified')
}
- public getURL(): string {
- const windowUrl = this._url ?? this._config.defaultURL
- if (this._sharedConfig.baseURL && !windowUrl.match(/^(?:[a-z+]+:)?\/\//i)) {
- // URL does not look absolute, add the baseURL
- return urlJoin(this._sharedConfig.baseURL, windowUrl)
- } else {
- return windowUrl
- }
+ private _setupWebContentListeners() {
+ this.window.webContents.off('render-process-gone', this._handleRenderProcessGone)
+ this.window.webContents.on('render-process-gone', this._handleRenderProcessGone)
+
+ this.window.webContents.off('console-message', this._handleConsoleMessage)
+ this.window.webContents.on('console-message', this._handleConsoleMessage)
}
- private handleRenderProcessGone = (event: Electron.Event, details: Electron.RenderProcessGoneDetails): void => {
+ private _handleRenderProcessGone = (event: Electron.Event, details: Electron.RenderProcessGoneDetails): void => {
if (details.reason !== 'clean-exit') {
this.status = {
statusCode: StatusCode.ERROR,
@@ -303,7 +335,7 @@ export class WindowHelper extends EventEmitter {
}
}
}
- private handleConsoleMessage = (
+ private _handleConsoleMessage = (
_event: Electron.Event,
level: number,
message: string,
@@ -315,15 +347,7 @@ export class WindowHelper extends EventEmitter {
this.logger.debug(`${this.id}: ${logMessage}`)
}
}
-
- private setupWebContentListeners() {
- this.window.webContents.off('render-process-gone', this.handleRenderProcessGone)
- this.window.webContents.on('render-process-gone', this.handleRenderProcessGone)
-
- this.window.webContents.off('console-message', this.handleConsoleMessage)
- this.window.webContents.on('console-message', this.handleConsoleMessage)
- }
- private async displayDebugOverlay() {
+ private async _displayDebugOverlay() {
await this.window.webContents.executeJavaScript(`
function setupMonitor() {
var overlay = document.createElement('div');
@@ -359,7 +383,7 @@ function setupMonitor() {
}
setupMonitor();`)
}
- private async injectUpdateCSS() {
+ private async _injectUpdateCSS() {
if (this.config.hideCursor ?? true) {
// Hide cursor:
// This should hide the cursor for most elements that sets their own cursor
diff --git a/src/lib/queue.ts b/src/lib/queue.ts
new file mode 100644
index 0000000..4356a47
--- /dev/null
+++ b/src/lib/queue.ts
@@ -0,0 +1,72 @@
+/**
+ * Super simple queue, that allows you to bind a method to a queue and it will run them in order.
+ */
+export class Queue {
+ private queue: {
+ isRunning: boolean
+ fcn: () => Promise
+ resolve: () => void
+ reject: (err: Error) => void
+ }[] = []
+
+ public bindMethod Promise>(
+ fcn: T,
+ clearWaiting?: {
+ reason: string
+ }
+ ): T {
+ return (async (...args: any[]) => {
+ await new Promise((resolve, reject) => {
+ if (clearWaiting) {
+ this.clearWaiting(clearWaiting.reason)
+ }
+
+ this.queue.push({
+ isRunning: false,
+ fcn: async () => {
+ return fcn(...args)
+ },
+ resolve,
+ reject,
+ })
+
+ this.checkQueue()
+ })
+ }) as T
+ }
+
+ /**
+ * Clear the queue from any not-yet-running functions and rejected them with the given reason.
+ * Any functions that are already running will not be affected.
+ */
+ public clearWaiting(reason: string): void {
+ for (const q of this.queue) {
+ if (q.isRunning) continue
+
+ q.reject(new Error(`Aborted, due to reason: "${reason}"`))
+ }
+ // Clear the queue, but leave any isRunning functions in place:
+ this.queue = this.queue.filter((q) => q.isRunning)
+ }
+
+ private checkQueue() {
+ const nextInQueue = this.queue[0]
+ if (!nextInQueue) return
+ if (nextInQueue.isRunning) return
+
+ nextInQueue.isRunning = true
+
+ nextInQueue
+ .fcn()
+ .then(() => {
+ nextInQueue.resolve()
+ })
+ .catch((err) => {
+ nextInQueue.reject(err)
+ })
+ .finally(() => {
+ this.queue.shift()
+ this.checkQueue()
+ })
+ }
+}