Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype: Run debuggable blueprints as separate process SOFIE-1548 #985

Closed
wants to merge 11 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
wip: studio.validateConfig works
Julusian committed Jul 10, 2023

Verified

This commit was signed with the committer’s verified signature.
Julusian Julian Waller
commit 02f63ce61a3ca3f2d14e342369aed377afbb9d0b
85 changes: 84 additions & 1 deletion meteor/yarn.lock
Original file line number Diff line number Diff line change
@@ -1584,6 +1584,13 @@ __metadata:
languageName: node
linkType: hard

"@socket.io/component-emitter@npm:~3.1.0":
version: 3.1.0
resolution: "@socket.io/component-emitter@npm:3.1.0"
checksum: db069d95425b419de1514dffe945cc439795f6a8ef5b9465715acf5b8b50798e2c91b8719cbf5434b3fe7de179d6cdcd503c277b7871cb3dd03febb69bdd50fa
languageName: node
linkType: hard

"@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A.":
version: 0.0.0-use.local
resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A."
@@ -1594,6 +1601,16 @@ __metadata:
languageName: node
linkType: soft

"@sofie-automation/blueprints-proxy@portal:../packages/blueprints-proxy::locator=automation-core%40workspace%3A.":
version: 0.0.0-use.local
resolution: "@sofie-automation/blueprints-proxy@portal:../packages/blueprints-proxy::locator=automation-core%40workspace%3A."
dependencies:
"@sofie-automation/blueprints-integration": 1.51.0-in-development
tslib: ^2.6.0
type-fest: ^3.10.0
languageName: node
linkType: soft

"@sofie-automation/code-standard-preset@npm:~2.4.7":
version: 2.4.7
resolution: "@sofie-automation/code-standard-preset@npm:2.4.7"
@@ -1662,6 +1679,7 @@ __metadata:
dependencies:
"@slack/webhook": ^6.1.0
"@sofie-automation/blueprints-integration": 1.51.0-in-development
"@sofie-automation/blueprints-proxy": 1.51.0-in-development
"@sofie-automation/corelib": 1.51.0-in-development
"@sofie-automation/shared-lib": 1.51.0-in-development
amqplib: ^0.10.3
@@ -1671,6 +1689,7 @@ __metadata:
mongodb: ^5.5.0
p-lazy: ^3.1.0
p-timeout: ^4.1.0
socket.io-client: ^4.7.1
superfly-timeline: ^8.3.1
threadedclass: ^1.2.1
tslib: ^2.6.0
@@ -4589,7 +4608,7 @@ __metadata:
languageName: node
linkType: hard

"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4":
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2":
version: 4.3.4
resolution: "debug@npm:4.3.4"
dependencies:
@@ -5108,6 +5127,26 @@ __metadata:
languageName: node
linkType: hard

"engine.io-client@npm:~6.5.1":
version: 6.5.1
resolution: "engine.io-client@npm:6.5.1"
dependencies:
"@socket.io/component-emitter": ~3.1.0
debug: ~4.3.1
engine.io-parser: ~5.1.0
ws: ~8.11.0
xmlhttprequest-ssl: ~2.0.0
checksum: 411a4f1d2ef8c624fa2d6499ea31bc1da9609e0aaa07aee6e0b713084ab534e1db02b76cf9e80a625bf8d4432ae6088438a40a3748e4746179753a639c5054dc
languageName: node
linkType: hard

"engine.io-parser@npm:~5.1.0":
version: 5.1.0
resolution: "engine.io-parser@npm:5.1.0"
checksum: a15fc0ba5d5fc5fb2c3029de1826538970463d0fa5c04d8dc2c72aabde92f1c923a9de409962490c3204da7245704286f9fb0ed4e5d71b55a6b035945f64c281
languageName: node
linkType: hard

"ensure-type@npm:^1.5.0":
version: 1.5.1
resolution: "ensure-type@npm:1.5.1"
@@ -11756,6 +11795,28 @@ __metadata:
languageName: node
linkType: hard

"socket.io-client@npm:^4.7.1":
version: 4.7.1
resolution: "socket.io-client@npm:4.7.1"
dependencies:
"@socket.io/component-emitter": ~3.1.0
debug: ~4.3.2
engine.io-client: ~6.5.1
socket.io-parser: ~4.2.4
checksum: 5e606ebe01eab4a034ef982b2fc9936a6d98ce9fa7940dd7dcd93f1473a8c273ee69d045c087ac534f0d232285e81c16644de4f28d1731ee864402a9ee3059ee
languageName: node
linkType: hard

"socket.io-parser@npm:~4.2.4":
version: 4.2.4
resolution: "socket.io-parser@npm:4.2.4"
dependencies:
"@socket.io/component-emitter": ~3.1.0
debug: ~4.3.1
checksum: 61540ef99af33e6a562b9effe0fad769bcb7ec6a301aba5a64b3a8bccb611a0abdbe25f469933ab80072582006a78ca136bf0ad8adff9c77c9953581285e2263
languageName: node
linkType: hard

"socks-proxy-agent@npm:^7.0.0":
version: 7.0.0
resolution: "socks-proxy-agent@npm:7.0.0"
@@ -13453,6 +13514,21 @@ __metadata:
languageName: node
linkType: hard

"ws@npm:~8.11.0":
version: 8.11.0
resolution: "ws@npm:8.11.0"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: 316b33aba32f317cd217df66dbfc5b281a2f09ff36815de222bc859e3424d83766d9eb2bd4d667de658b6ab7be151f258318fb1da812416b30be13103e5b5c67
languageName: node
linkType: hard

"xml-js@npm:^1.6.11":
version: 1.6.11
resolution: "xml-js@npm:1.6.11"
@@ -13502,6 +13578,13 @@ __metadata:
languageName: node
linkType: hard

"xmlhttprequest-ssl@npm:~2.0.0":
version: 2.0.0
resolution: "xmlhttprequest-ssl@npm:2.0.0"
checksum: 1e98df67f004fec15754392a131343ea92e6ab5ac4d77e842378c5c4e4fd5b6a9134b169d96842cc19422d77b1606b8df84a5685562b3b698cb68441636f827e
languageName: node
linkType: hard

"xtend@npm:^4.0.2, xtend@npm:~4.0.0, xtend@npm:~4.0.1":
version: 4.0.2
resolution: "xtend@npm:4.0.2"
1 change: 1 addition & 0 deletions packages/blueprints-proxy/package.json
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@
],
"dependencies": {
"@sofie-automation/blueprints-integration": "1.51.0-in-development",
"socket.io": "^4.7.1",
"tslib": "^2.6.0",
"type-fest": "^3.10.0"
},
57 changes: 57 additions & 0 deletions packages/blueprints-proxy/src/context/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ICommonContext, NoteSeverity } from '@sofie-automation/blueprints-integration'
import * as crypto from 'crypto'

function getHash(str: string): string {
const hash = crypto.createHash('sha1')
return hash.update(str).digest('base64').replace(/[+/=]/g, '_') // remove +/= from strings, because they cause troubles
}

export class CommonContext implements ICommonContext {
private readonly _contextName: string

private hashI = 0
private hashed: { [hash: string]: string } = {}

constructor(identifier: string) {
this._contextName = identifier
}
getHashId(str: string, isNotUnique?: boolean): string {
if (!str) str = 'hash' + this.hashI++

if (isNotUnique) {
str = str + '_' + this.hashI++
}

const id = getHash(this._contextName + '_' + str.toString()) // TODO - is this unique enough?
this.hashed[id] = str
return id
}
unhashId(hash: string): string {
return this.hashed[hash] || hash
}

logDebug(message: string): void {
console.debug(`"${this._contextName}": "${message}"`)
}
logInfo(message: string): void {
console.info(`"${this._contextName}": "${message}"`)
}
logWarning(message: string): void {
console.warn(`"${this._contextName}": "${message}"`)
}
logError(message: string): void {
console.error(`"${this._contextName}": "${message}"`)
}
protected logNote(message: string, type: NoteSeverity): void {
if (type === NoteSeverity.ERROR) {
this.logError(message)
} else if (type === NoteSeverity.WARNING) {
this.logWarning(message)
} else if (type === NoteSeverity.INFO) {
this.logInfo(message)
} else {
// assertNever(type)
this.logDebug(message)
}
}
}
49 changes: 49 additions & 0 deletions packages/blueprints-proxy/src/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Signature for the handler functions
*/
type HandlerFunction<T extends (...args: any) => any> = (
functionId: string,
...args: Parameters<T>
) => Promise<ReturnType<T>>

type HandlerFunctionOrNever<T> = T extends (...args: any) => any ? HandlerFunction<T> : never

/** Map of handler functions */
export type EventHandlers<T extends object> = {
[K in keyof T]: HandlerFunctionOrNever<T[K]>
}

export type ResultCallback<T> = (err: any, res: T) => void

/** Subscribe to all the events defined in the handlers, and wrap with safety and logging */
export function listenToEvents<T extends object>(socket: any, handlers: EventHandlers<T>): void {
// const logger = createChildLogger(`module/${connectionId}`);

for (const [event, handler] of Object.entries(handlers)) {
socket.on(event as any, async (functionId: string, msg: any, cb: ResultCallback<any>) => {
if (!functionId || typeof functionId !== 'string') {
console.warn(`Received malformed functionId "${event}"`)
return // Ignore messages without correct structure
}
if (!msg || typeof msg !== 'object') {
console.warn(`Received malformed message object "${event}"`)
return // Ignore messages without correct structure
}
if (cb && typeof cb !== 'function') {
console.warn(`Received malformed callback "${event}"`)
return // Ignore messages without correct structure
}

try {
// Run it
const handler2 = handler as HandlerFunction<(msg: any) => any>
const result = await handler2(functionId, msg)

if (cb) cb(null, result)
} catch (e: any) {
console.error(`Command failed: ${e}`, e.stack)
if (cb) cb(e?.toString() ?? JSON.stringify(e), undefined)
}
})
}
}
62 changes: 62 additions & 0 deletions packages/blueprints-proxy/src/host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ShowStyleBlueprintManifest, StudioBlueprintManifest } from '@sofie-automation/blueprints-integration'
// import { proxyStudioBlueprint } from './blueprint/studio'
// import { klona } from 'klona/full'
import { createServer } from 'http'
import { ClientToServerEvents, ServerToClientEvents } from './index'
import { Server } from 'socket.io'
import { listenToEvents } from './helper'
import { studio_validateConfig } from './routers/studio/config'

export function runForBlueprints(
studioBlueprint: StudioBlueprintManifest<any, any>,
_showStyleBlueprint: ShowStyleBlueprintManifest<any, any>
): void {
// // Clone blueprint and replace any methods with their proxy versions
// const proxiedStudioBlueprint = klona(studioBlueprint)
// for (const [key, value] of Object.entries<any>(proxiedStudioBlueprint)) {
// if (typeof value === 'function') {
// // @ts-expect-error key fails
// if (!proxyStudioBlueprint[key]) {
// throw new Error(`Missing key in studio proxy: ${key}`)
// }

// // @ts-expect-error key fails
// proxiedStudioBlueprint[key] = proxyStudioBlueprint[key]
// }
// }

const httpServer = createServer()
const io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer, {
cors: {
// Allow everything
origin: (o, cb) => cb(null, o),
credentials: true,
},
// options
})

io.on('connection', (socket) => {
// ...
console.log(`connection from ${socket.id}`)

// subscribe to socket events from host
listenToEvents<ClientToServerEvents>(socket, {
// init: this._handleInit.bind(this),
// destroy: this._handleDestroy.bind(this),
// updateConfig: this._handleConfigUpdate.bind(this),
// executeAction: this._handleExecuteAction.bind(this),
// updateFeedbacks: this._handleUpdateFeedbacks.bind(this),
// updateActions: this._handleUpdateActions.bind(this),
// getConfigFields: this._handleGetConfigFields.bind(this),
// handleHttpRequest: this._handleHttpRequest.bind(this),
// learnAction: this._handleLearnAction.bind(this),
// learnFeedback: this._handleLearnFeedback.bind(this),
// startStopRecordActions: this._handleStartStopRecordActions.bind(this),
studio_validateConfig: async (...args) => studio_validateConfig(studioBlueprint, socket, ...args),
})
})

httpServer.listen(2345, () => {
console.log('Started server')
})
}
8 changes: 7 additions & 1 deletion packages/blueprints-proxy/src/index.ts
Original file line number Diff line number Diff line change
@@ -10,5 +10,11 @@ export interface ServerToClientEvents {

export interface ClientToServerEvents {
// hello: () => void
studio_validateConfig: (functionId: string, identifier: string, config: IBlueprintConfig) => IConfigMessage[]
studio_validateConfig: (msg: StudioValidateConfigArgs) => StudioValidateConfigResult
}

export interface StudioValidateConfigArgs {
identifier: string
config: IBlueprintConfig
}
export type StudioValidateConfigResult = IConfigMessage[]
17 changes: 17 additions & 0 deletions packages/blueprints-proxy/src/routers/studio/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { IConfigMessage, StudioBlueprintManifest } from '@sofie-automation/blueprints-integration'
import { CommonContext } from '../../context/common'
import type { StudioValidateConfigArgs } from '../../index'
import { MySocket } from '../util'

export async function studio_validateConfig(
studioBlueprint: StudioBlueprintManifest,
_socket: MySocket,
_id: string,
msg: StudioValidateConfigArgs
): Promise<IConfigMessage[]> {
if (!studioBlueprint.validateConfig) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not..

const context = new CommonContext(`validateConfig ${msg.identifier}`)

return studioBlueprint.validateConfig(context, msg.config)
}
4 changes: 4 additions & 0 deletions packages/blueprints-proxy/src/routers/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Socket } from 'socket.io'
import { ClientToServerEvents, ServerToClientEvents } from '..'

export type MySocket = Socket<ClientToServerEvents, ServerToClientEvents>
Loading