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

feat: modules as plugins #1002 #3062

Draft
wants to merge 67 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
21449c9
chore: refactor InstanceModules to not extend CoreBase
Julusian Sep 24, 2024
b0352e4
wip
Julusian Sep 24, 2024
7a22823
wip
Julusian Sep 24, 2024
778fe64
wip: something ui
Julusian Feb 20, 2024
c1cea2f
wip: remove separation between legacy and bundled
Julusian Sep 24, 2024
c26a2a3
wip: pump more data to the ui
Julusian Feb 21, 2024
6eac65c
wip: Some ui
Julusian Feb 24, 2024
414c856
wip: list versions in ui
Julusian Feb 24, 2024
4b3ff8b
wip
Julusian Sep 24, 2024
0d87cdb
wip:
Julusian Feb 24, 2024
c0bf8ca
wip: change to single dev module
Julusian Sep 24, 2024
5382802
wip: something
Julusian Sep 24, 2024
5b864c6
wip: attempt at ui for adding modules
Julusian Sep 24, 2024
00a417f
wip: better
Julusian Sep 24, 2024
6bee0df
wip: try using specific version when adding
Julusian Sep 24, 2024
78aa482
wip: reworking (again)
Julusian Sep 24, 2024
1e0e4b1
wip: choose module versions to use
Julusian Sep 24, 2024
d88d5ea
wip: config fixup
Julusian Sep 24, 2024
f4632a6
fix: incompat warning
Julusian Sep 24, 2024
bdc0b23
wip: fix help modal
Julusian Sep 25, 2024
d3f195f
wip: fixup module list page a bit
Julusian Sep 25, 2024
6921456
wip: alternate list of all modules
Julusian Sep 30, 2024
bab6457
wip: first version of importing custom modules
Julusian Sep 30, 2024
54fc052
wip
Julusian Sep 30, 2024
bdaad8d
wip: adding and removing custom modules
Julusian Oct 1, 2024
3ecf71b
wip: fix module adding
Julusian Oct 1, 2024
48342ed
wip: present as proper table
Julusian Oct 9, 2024
e97ab6f
fix: add warning
Julusian Oct 9, 2024
08cf08b
wip: start of backend to scrape module store
Julusian Oct 9, 2024
9d8ab09
wip: crude start of module discover ui
Julusian Oct 9, 2024
5397fa4
wip: some crude ui to install, backend to install modules
Julusian Oct 10, 2024
ee39d79
wip: some more flow
Julusian Oct 10, 2024
28b7ae7
fix: reimplement reloading dev module (untested)
Julusian Oct 10, 2024
0800681
wip: new ui design
Julusian Oct 12, 2024
ed4775c
wip: add import module button
Julusian Oct 12, 2024
3d73293
wip: new ui to discover and install modules
Julusian Oct 12, 2024
3d5d588
wip: some refinement of modules discover tab
Julusian Oct 12, 2024
d54365a
wip: move cache storage into cache
Julusian Oct 12, 2024
98256a0
wip: some ui boilerplate
Julusian Oct 12, 2024
d9f6efd
wip: remove old modules poc ui
Julusian Oct 12, 2024
4616dc8
wip: change types
Julusian Oct 12, 2024
cdf7aff
wip: remove unused files
Julusian Oct 12, 2024
ceb533b
wip: start of per module data fetching
Julusian Oct 12, 2024
3351884
wip: start of module manage page
Julusian Oct 12, 2024
6c049b6
wip: implement things a little more
Julusian Oct 13, 2024
7d8969c
wip
Julusian Oct 13, 2024
93f7a40
wip: better tables
Julusian Oct 13, 2024
037f5ba
wip: refactor visibility header buttons to avoid duplication
Julusian Oct 13, 2024
31cc6f0
wip: versions visibility
Julusian Oct 13, 2024
1eedbb8
wip
Julusian Oct 13, 2024
266ba7e
wip: refactor
Julusian Oct 13, 2024
f8b54a4
wip: add file on disk to indicate that store version is a prerelease
Julusian Oct 13, 2024
8a14f0f
wip: docs
Julusian Oct 13, 2024
0fb70c0
fix: module filter producing bad results
Julusian Oct 13, 2024
cb6e62a
wip: tidy
Julusian Oct 13, 2024
cd80be9
wip
Julusian Oct 15, 2024
c3cb411
wip: use url routing for modules manager
Julusian Oct 15, 2024
08c72e7
wip: some better error handling
Julusian Oct 15, 2024
0b55ef9
wip: improve refresh and last updated styling
Julusian Oct 15, 2024
efe0e8d
wip: format versions table dates better
Julusian Oct 15, 2024
9b58c11
wip: better error handling
Julusian Oct 15, 2024
67c4eaf
wip: module download headers
Julusian Oct 15, 2024
88560d3
wip: implement install latest
Julusian Oct 15, 2024
1d5ae99
wip: move modules into shared dir
Julusian Oct 15, 2024
c27581e
wip: split out file
Julusian Oct 16, 2024
254ac0a
wip: install when adding
Julusian Oct 19, 2024
4fbe42d
wip: remove separation of custom versions
Julusian Oct 22, 2024
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
7 changes: 6 additions & 1 deletion companion/lib/Data/ImportExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,12 @@ export class DataImportExport extends CoreBase {
} else {
// Create a new instance
const instance_type = this.instance.modules.verifyInstanceTypeIsCurrent(obj.instance_type)
const [newId, newConfig] = this.instance.addInstanceWithLabel({ type: instance_type }, obj.label, true)
const [newId, newConfig] = this.instance.addInstanceWithLabel(
{ type: instance_type },
obj.label,
{ mode: 'stable', id: null }, // Always create using the latest stable
true
)
console.log('created', instance_type, newId)
if (newId && newConfig) {
this.instance.setInstanceLabelAndConfig(newId, null, 'config' in obj ? obj.config : null)
Expand Down
4 changes: 2 additions & 2 deletions companion/lib/Data/StoreBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ export abstract class DataStoreBase {
* @param table - the table to get from
* @returns the rows
*/
public getTable(table: string): any {
let out = {}
public getTable(table: string): Record<string, any> {
let out: Record<string, any> = {}

if (table.length > 0) {
const query = this.store.prepare(`SELECT id, value FROM ${table}`)
Expand Down
6 changes: 6 additions & 0 deletions companion/lib/Instance/ConnectionConfigStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { nanoid } from 'nanoid'
import { cloneDeep } from 'lodash-es'
import { ClientConnectionConfig } from '@companion-app/shared/Model/Common.js'
import { makeLabelSafe } from '@companion-app/shared/Label.js'
import { ModuleVersionInfo } from '@companion-app/shared/Model/ModuleInfo.js'

export class ConnectionConfigStore {
// readonly #logger = LogController.createLogger('Instance/ConnectionConfigStore')
Expand Down Expand Up @@ -55,6 +56,7 @@ export class ConnectionConfigStore {
moduleType: string,
label: string,
product: string | undefined,
moduleVersion: ModuleVersionInfo,
disabled: boolean
): [id: string, config: ConnectionConfig] {
// Find the highest rank given to an instance
Expand All @@ -70,6 +72,8 @@ export class ConnectionConfigStore {

this.#store[id] = {
instance_type: moduleType,
moduleVersionMode: moduleVersion.mode,
moduleVersionId: moduleVersion?.id || null,
sortOrder: highestRank + 1,
label: label,
isFirstInit: true,
Expand Down Expand Up @@ -102,6 +106,8 @@ export class ConnectionConfigStore {

result[id] = {
instance_type: config.instance_type,
moduleVersionMode: config.moduleVersionMode,
moduleVersionId: config.moduleVersionId,
label: config.label,
enabled: config.enabled,
sortOrder: config.sortOrder,
Expand Down
131 changes: 108 additions & 23 deletions companion/lib/Instance/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ import type { ModuleManifest } from '@companion-module/base'
import type { ExportInstanceFullv4, ExportInstanceMinimalv4 } from '@companion-app/shared/Model/ExportModel.js'
import type { ClientSocket } from '../UI/Handler.js'
import { ConnectionConfigStore } from './ConnectionConfigStore.js'
import { InstanceInstalledModulesManager } from './InstalledModulesManager.js'
import type { ModuleVersionInfo } from '@companion-app/shared/Model/ModuleInfo.js'
import type { ModuleDirs } from './Types.js'
import path from 'path'
import { isPackaged } from '../Resources/Util.js'
import { fileURLToPath } from 'url'
import { ModuleStoreService } from './ModuleStore.js'

const InstancesRoom = 'instances'

Expand All @@ -58,6 +65,8 @@ export class InstanceController extends CoreBase<InstanceControllerEvents> {
readonly status: InstanceStatus
readonly moduleHost: ModuleHost
readonly modules: InstanceModules
readonly modulesStore: ModuleStoreService
readonly userModulesManager: InstanceInstalledModulesManager

constructor(registry: Registry) {
super(registry, 'Instance/Controller')
Expand All @@ -67,10 +76,31 @@ export class InstanceController extends CoreBase<InstanceControllerEvents> {

this.#configStore = new ConnectionConfigStore(registry.db, this.broadcastChanges.bind(this))

function generatePath(subpath: string): string {
if (isPackaged()) {
return path.join(__dirname, subpath)
} else {
return fileURLToPath(new URL(path.join('../../..', subpath), import.meta.url))
}
}

const moduleDirs: ModuleDirs = {
bundledLegacyModulesDir: path.resolve(generatePath('modules')),
bundledModulesDir: path.resolve(generatePath('bundled-modules')),
installedModulesDir: path.join(registry.appInfo.modulesDir, 'store'),
}

this.definitions = new InstanceDefinitions(registry)
this.status = new InstanceStatus(registry.io, registry.controls)
this.moduleHost = new ModuleHost(registry, this.status, this.#configStore)
this.modules = new InstanceModules(registry)
this.modules = new InstanceModules(registry.io, registry.api_router, this, moduleDirs)
this.modulesStore = new ModuleStoreService(registry.io, registry.data.cache)
this.userModulesManager = new InstanceInstalledModulesManager(
registry.appInfo,
this.modules,
this.modulesStore,
moduleDirs
)

// Prepare for clients already
this.broadcastChanges(this.#configStore.getAllInstanceIds())
Expand Down Expand Up @@ -104,19 +134,22 @@ export class InstanceController extends CoreBase<InstanceControllerEvents> {
* @param extraModulePath - extra directory to search for modules
*/
async initInstances(extraModulePath: string): Promise<void> {
const connectionIds = this.#configStore.getAllInstanceIds()
this.logger.silly('instance_init', connectionIds)
await this.userModulesManager.init()

await this.modules.initInstances(extraModulePath)

const connectionIds = this.#configStore.getAllInstanceIds()
this.logger.silly('instance_init', connectionIds)
for (const id of connectionIds) {
this.#activate_module(id, false)
}

this.emit('connection_added')
}

reloadUsesOfModule(moduleId: string): void {
async reloadUsesOfModule(moduleId: string, mode: 'release' | 'dev', versionId: string | null): Promise<void> {
// TODO - use the version!

// restart usages of this module
const { connectionIds, labels } = this.#configStore.findActiveUsagesOfModule(moduleId)
for (const id of connectionIds) {
Expand Down Expand Up @@ -180,36 +213,28 @@ export class InstanceController extends CoreBase<InstanceControllerEvents> {
this.logger.debug(`instance "${connectionConfig.label}" configuration updated`)
}

/**
* Add a new instance of a module
*/
addInstance(data: CreateConnectionData, disabled: boolean): string {
let module = data.type

const moduleInfo = this.modules.getModuleManifest(module)
if (!moduleInfo) throw new Error(`Unknown module type ${module}`)

return this.addInstanceWithLabel(data, moduleInfo.display.shortname, disabled)[0]
}

/**
* Add a new instance of a module with a predetermined label
*/
addInstanceWithLabel(
data: CreateConnectionData,
labelBase: string,
version: ModuleVersionInfo,
disabled: boolean
): [id: string, config: ConnectionConfig] {
let module = data.type
let product = data.product

const moduleInfo = this.modules.getModuleManifest(module, version.mode, version.id)
if (!moduleInfo) throw new Error(`Unknown module type ${module}`)

const label = this.#configStore.makeLabelUnique(labelBase)

if (this.getIdForLabel(label)) throw new Error(`Label "${label}" already in use`)

this.logger.info('Adding connection ' + module + ' ' + product)

const [id, config] = this.#configStore.addConnection(module, label, product, disabled)
const [id, config] = this.#configStore.addConnection(module, label, product, version, disabled)

this.#activate_module(id, true)

Expand All @@ -233,7 +258,11 @@ export class InstanceController extends CoreBase<InstanceControllerEvents> {
const config = this.#configStore.getConfigForId(id)
if (!config) return undefined

const moduleManifest = this.modules.getModuleManifest(config.instance_type)
const moduleManifest = this.modules.getModuleManifest(
config.instance_type,
config.moduleVersionMode,
config.moduleVersionId
)

return moduleManifest?.manifest
}
Expand Down Expand Up @@ -394,6 +423,12 @@ export class InstanceController extends CoreBase<InstanceControllerEvents> {

config.instance_type = this.modules.verifyInstanceTypeIsCurrent(config.instance_type)

// Seamless fixup old configs
if (config.moduleVersionMode === undefined) {
config.moduleVersionMode = 'stable'
config.moduleVersionId = null
}

if (config.enabled === false) {
this.logger.silly("Won't load disabled module " + id + ' (' + config.instance_type + ')')
this.status.updateInstanceStatus(id, null, 'Disabled')
Expand All @@ -412,9 +447,20 @@ export class InstanceController extends CoreBase<InstanceControllerEvents> {
// TODO this could check if anything above changed, or is_being_created
this.#configStore.commitChanges([id])

const moduleInfo = this.modules.getModuleManifest(config.instance_type)
const moduleInfo = this.modules.getModuleManifest(
config.instance_type,
config.moduleVersionMode,
config.moduleVersionId
)
if (!moduleInfo) {
this.logger.error('Configured instance ' + config.instance_type + ' could not be loaded, unknown module')
if (this.modules.hasModule(config.instance_type)) {
// TODO - check level
this.status.updateInstanceStatus(id, null, 'Unknown module version')
} else {
// TODO - check level
this.status.updateInstanceStatus(id, null, 'Unknown module')
}
} else {
this.moduleHost.queueRestartConnection(id, config, moduleInfo).catch((e) => {
this.logger.error('Configured instance ' + config.instance_type + ' failed to start: ', e)
Expand All @@ -430,6 +476,8 @@ export class InstanceController extends CoreBase<InstanceControllerEvents> {
this.definitions.clientConnect(client)
this.status.clientConnect(client)
this.modules.clientConnect(client)
this.modulesStore.clientConnect(client)
this.userModulesManager.clientConnect(client)

client.onPromise('connections:subscribe', () => {
client.join(InstancesRoom)
Expand Down Expand Up @@ -462,7 +510,7 @@ export class InstanceController extends CoreBase<InstanceControllerEvents> {
}
})

client.onPromise('connections:set-config', (id, label, config) => {
client.onPromise('connections:set-label-and-config', (id, label, config) => {
const idUsingLabel = this.getIdForLabel(label)
if (idUsingLabel && idUsingLabel !== id) {
return 'duplicate label'
Expand All @@ -477,6 +525,43 @@ export class InstanceController extends CoreBase<InstanceControllerEvents> {
return null
})

client.onPromise('connections:set-label-and-version', (id, label, version) => {
const idUsingLabel = this.getIdForLabel(label)
if (idUsingLabel && idUsingLabel !== id) {
return 'duplicate label'
}

if (!isLabelValid(label)) {
return 'invalid label'
}

// TODO - refactor/optimise/tidy this

this.setInstanceLabelAndConfig(id, label, null)

const config = this.#configStore.getConfigForId(id)
if (!config) return 'no connection'

const moduleInfo = this.modules.getModuleManifest(config.instance_type, version.mode, version.id)
if (!moduleInfo)
throw new Error(`Unknown module type or version ${config.instance_type} (${version.mode}:${version.id})`)

// Update the config
config.moduleVersionMode = version.mode
config.moduleVersionId = version.id
this.#configStore.commitChanges([id])

console.log('new config', config)

// Trigger a restart
if (config.enabled) {
this.enableDisableInstance(id, false)
this.enableDisableInstance(id, true)
}

return null
})

client.onPromise('connections:set-enabled', (id, state) => {
this.enableDisableInstance(id, !!state)
})
Expand All @@ -485,9 +570,9 @@ export class InstanceController extends CoreBase<InstanceControllerEvents> {
await this.deleteInstance(id)
})

client.onPromise('connections:add', (module) => {
const id = this.addInstance(module, false)
return id
client.onPromise('connections:add', (module, label, version) => {
const connectionInfo = this.addInstanceWithLabel(module, label, version, false)
return connectionInfo[0]
})

client.onPromise('connections:set-order', async (connectionIds) => {
Expand Down
12 changes: 9 additions & 3 deletions companion/lib/Instance/Host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { RespawnMonitor } from '@companion-app/shared/Respawn.js'
import type { Registry } from '../Registry.js'
import type { InstanceStatus } from './Status.js'
import type { ConnectionConfig } from '@companion-app/shared/Model/Connections.js'
import type { ModuleInfo } from './Modules.js'
import type { ConnectionConfigStore } from './ConnectionConfigStore.js'
import { isModuleApiVersionCompatible } from '@companion-app/shared/ModuleApiVersionCheck.js'
import type { SomeModuleVersionInfo } from './Types.js'

/**
* A backoff sleep strategy
Expand Down Expand Up @@ -93,7 +93,13 @@ export class ModuleHost {
if (!child.crashed) {
child.crashed = setTimeout(() => {
const config = this.#connectionConfigStore.getConfigForId(child.connectionId)
const moduleInfo = config && this.#registry.instance.modules.getModuleManifest(config.instance_type)
const moduleInfo =
config &&
this.#registry.instance.modules.getModuleManifest(
config.instance_type,
config.moduleVersionMode,
config.moduleVersionId
)

// Restart after a short sleep
this.queueRestartConnection(child.connectionId, config, moduleInfo)
Expand Down Expand Up @@ -331,7 +337,7 @@ export class ModuleHost {
async queueRestartConnection(
connectionId: string,
config: ConnectionConfig | undefined,
moduleInfo: ModuleInfo | undefined
moduleInfo: SomeModuleVersionInfo | undefined
): Promise<void> {
if (!config || !moduleInfo) return

Expand Down
Loading
Loading