Skip to content

Commit

Permalink
feat: Bonjour device discovery config field #2087 (#2428)
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian committed Aug 12, 2023
1 parent 4750747 commit 836bed8
Show file tree
Hide file tree
Showing 10 changed files with 397 additions and 23 deletions.
27 changes: 6 additions & 21 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on: [push]
jobs:
linux64:
runs-on: ubuntu-20.04
outputs:
version: ${{ steps.filenames.outputs.longversion }}
do-docker: ${{ steps.upload.outputs.branch }}
steps:
- uses: actions/checkout@v3
with:
Expand Down Expand Up @@ -67,13 +70,6 @@ jobs:
api-target: 'linux-tgz'
api-secret: ${{ secrets.BITFOCUS_API_PROJECT_SECRET }}

- name: Report docker build is needed
if: ${{ steps.upload.outputs.branch }}
uses: actions/upload-artifact@v3
with:
name: do-docker-build
path: dist/BUILD

linux-arm64:
runs-on: ubuntu-20.04
steps:
Expand Down Expand Up @@ -345,25 +341,14 @@ jobs:
IMAGE_NAME: companion

steps:
- name: Check if build is required
id: build-check
uses: xSAVIKx/artifact-exists-action@v0
with:
name: do-docker-build

- uses: actions/checkout@v3
if: steps.build-check.outputs.exists == 'true'

- uses: actions/download-artifact@v3
if: steps.build-check.outputs.exists == 'true'
with:
name: do-docker-build
if: ${{ needs.linux64.outputs.do-docker }}

- name: Determine target image name
if: steps.build-check.outputs.exists == 'true'
if: ${{ needs.linux64.outputs.do-docker }}
id: image-name
run: |
BUILD_VERSION=$(cat BUILD)
BUILD_VERSION=${{ needs.linux64.outputs.version }}
IMAGE_ID=ghcr.io/${{ github.repository }}/$IMAGE_NAME
Expand Down
9 changes: 9 additions & 0 deletions lib/Instance/Controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,15 @@ class Instance extends CoreBase {
return undefined
}

getManifestForInstance(id) {
const config = this.store.db[id]
if (!config) return null

const moduleManifest = this.modules.getModuleManifest(config.instance_type)

return moduleManifest.manifest
}

enableDisableInstance(id, state) {
if (this.store.db[id]) {
const label = this.store.db[id].label
Expand Down
9 changes: 9 additions & 0 deletions lib/Instance/Modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,15 @@ class InstanceModules extends CoreBase {
return result
}

/**
*
* @access public
* @param {string} module_id
*/
getModuleManifest(module_id) {
return this.known_modules[module_id]
}

/**
* Load the help markdown file for a specified module_id
* @access public
Expand Down
184 changes: 184 additions & 0 deletions lib/Service/BonjourDiscovery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { isEqual } from 'lodash-es'
import ServiceBase from './Base.js'
import { Bonjour, Browser } from 'bonjour-service'
import { nanoid } from 'nanoid'

function BonjourRoom(id) {
return `bonjour:${id}`
}

/**
* Class providing Bonjour discovery for modules.
*
* @extends ServiceBonjour
* @author Håkon Nessjøen <haakon@bitfocus.io>
* @author Keith Rocheck <keith.rocheck@gmail.com>
* @author William Viker <william@bitfocus.io>
* @author Julian Waller <me@julusian.co.uk>
* @since 1.2.0
* @copyright 2022 Bitfocus AS
* @license
* This program is free software.
* You should have received a copy of the MIT licence as well as the Bitfocus
* Individual Contributor License Agreement for Companion along with
* this program.
*
* You can be released from the requirements of the license by purchasing
* a commercial license. Buying such a license is mandatory as soon as you
* develop commercial activities involving the Companion software without
* disclosing the source code of your own applications.
*/
class ServiceBonjourDiscovery extends ServiceBase {
/**
* Active browsers running
*/
#browsers = new Map()

/**
* @param {Registry} registry - the application core
*/
constructor(registry) {
super(registry, 'bonjour-discovery', 'Service/BonjourDiscovery', undefined, undefined)

this.init()
}

/**
* Start the service if it is not already running
* @access protected
*/
listen() {
if (this.server === undefined) {
try {
this.server = new Bonjour()

this.logger.info('Listening for Bonjour messages')
} catch (e) {
this.logger.error(`Could not launch: ${e.message}`)
}
}
}

/**
* Close the socket before deleting it
* @access protected
*/
close() {
this.server.destroy()
}

/**
* Setup a new socket client's events
* @param {SocketIO} client - the client socket
* @access public
*/
clientConnect(client) {
client.on('disconnect', () => {
// Ensure any sessions the client was part of are cleaned up
for (const subId of this.#browsers.keys()) {
this.#removeClientFromSession(client.id, subId)
}
})

client.onPromise('bonjour:subscribe', (connectionId, queryId) =>
this.#joinOrCreateSession(client, connectionId, queryId)
)
client.on('bonjour:unsubscribe', (subId) => this.#leaveSession(client, subId))
}

#convertService(id, svc) {
return {
subId: id,
fqdn: svc.fqdn,
name: svc.name,
port: svc.port,
// type: svc.type,
// protocol: svc.protocol,
// txt: svc.txt,
// host: svc.host,
addresses: svc.addresses,
}
}

#joinOrCreateSession(client, connectionId, queryId) {
const manifest = this.instance.getManifestForInstance(connectionId)
const bonjourQuery = manifest?.bonjourQueries?.[queryId]
if (!bonjourQuery) throw new Error('Missing bonjour query')

const filter = {
type: bonjourQuery.type,
protocol: bonjourQuery.protocol,
txt: bonjourQuery.txt,
}
if (typeof filter.type !== 'string' || !filter.type) throw new Error('Invalid type for bonjour query')
if (typeof filter.protocol !== 'string' || !filter.protocol) throw new Error('Invalid protocol for bonjour query')

// Find existing browser
for (const [id, session] of this.#browsers.entries()) {
if (isEqual(session.filter, filter)) {
session.clientIds.add(client.id)

client.join(BonjourRoom(id))
this.logger.info(`Client ${client.id} joined ${id}`)

// After this message, send already known services to the client
setImmediate(() => {
for (const svc of session.browser.services) {
client.emit(`bonjour:service:up`, this.#convertService(id, svc))
}
})

return id
}
}

// Create new browser
this.logger.info(`Starting discovery of: ${JSON.stringify(filter)}`)
const browser = this.server.find(filter)
const id = nanoid()
const room = BonjourRoom(id)
this.#browsers.set(id, {
browser,
filter,
clientIds: new Set([client.id]),
})

// Setup event handlers
browser.on('up', (svc) => {
this.io.emitToRoom(room, `bonjour:service:up`, this.#convertService(id, svc))
})
browser.on('down', (svc) => {
this.io.emitToRoom(room, `bonjour:service:down`, this.#convertService(id, svc))
})

// Report to client
client.join(room)
this.logger.info(`Client ${client.id} joined ${id}`)
return id
}

#leaveSession(client, subId) {
this.logger.info(`Client ${client.id} left ${subId}`)
client.leave(BonjourRoom(subId))

this.#removeClientFromSession(client.id, subId)
}

#removeClientFromSession(clientId, subId) {
const session = this.#browsers.get(subId)
if (!session || !session.clientIds.delete(clientId)) return

// Cleanup after a timeout, as restarting the same query immediately causes it to fail
setTimeout(() => {
if (this.#browsers.has(subId) && session.clientIds.size === 0) {
this.logger.info(`Stopping discovery of: ${JSON.stringify(session.filter)}`)

this.#browsers.delete(subId)

session.browser.stop()
}
}, 500)
}
}

export default ServiceBonjourDiscovery
12 changes: 12 additions & 0 deletions lib/Service/Controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ServiceApi from './Api.js'
import ServiceArtnet from './Artnet.js'
import ServiceBonjourDiscovery from './BonjourDiscovery.js'
import ServiceElgatoPlugin from './ElgatoPlugin.js'
import ServiceEmberPlus from './EmberPlus.js'
import ServiceHttps from './Https.js'
Expand Down Expand Up @@ -48,6 +49,7 @@ class ServiceController {
this.satellite = new ServiceSatellite(registry)
this.elgatoPlugin = new ServiceElgatoPlugin(registry)
this.videohubPanel = new ServiceVideohubPanel(registry)
this.bonjourDiscovery = new ServiceBonjourDiscovery(registry)
}

/**
Expand All @@ -58,6 +60,7 @@ class ServiceController {
*/
updateUserConfig(key, value) {
this.artnet.updateUserConfig(key, value)
this.bonjourDiscovery.updateUserConfig(key, value)
this.elgatoPlugin.updateUserConfig(key, value)
this.emberplus.updateUserConfig(key, value)
this.https.updateUserConfig(key, value)
Expand All @@ -69,6 +72,15 @@ class ServiceController {
this.udp.updateUserConfig(key, value)
this.videohubPanel.updateUserConfig(key, value)
}

/**
* Setup a new socket client's events
* @param {SocketIO} client - the client socket
* @access public
*/
clientConnect(client) {
this.bonjourDiscovery.clientConnect(client)
}
}

export default ServiceController
1 change: 1 addition & 0 deletions lib/UI/Handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ class UIHandler {
this.registry.surfaces.clientConnect(client)
this.registry.instance.clientConnect(client)
this.registry.cloud.clientConnect(client)
this.registry.services.clientConnect(client)

client.on('disconnect', () => {
this.logger.debug('socket ' + client.id + ' disconnected')
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@sentry/tracing": "^7.58.1",
"archiver": "^5.3.1",
"body-parser": "^1.20.2",
"bonjour-service": "^1.1.1",
"bufferutil": "^4.0.7",
"colord": "^2.9.3",
"commander": "^11.0.0",
Expand Down

0 comments on commit 836bed8

Please sign in to comment.