Skip to content

Commit

Permalink
try again with dbus-ts
Browse files Browse the repository at this point in the history
  • Loading branch information
bwp91 committed Aug 30, 2024
1 parent 3c5bb99 commit 2f8d567
Show file tree
Hide file tree
Showing 17 changed files with 1,994 additions and 3 deletions.
21 changes: 20 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@
"debug": "^4.3.6",
"fast-srp-hap": "^2.0.4",
"futoin-hkdf": "^1.5.3",
"long": "^5.2.3",
"node-persist": "^0.0.12",
"source-map-support": "^0.5.21",
"tweetnacl": "^1.0.3"
"tweetnacl": "^1.0.3",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@antfu/eslint-config": "^3.0.0",
Expand All @@ -73,6 +75,7 @@
"@types/plist": "^3.0.5",
"@types/semver": "^7.3.7",
"@types/source-map-support": "^0.5.10",
"@types/xml2js": "^0.4.14",
"@vitest/coverage-v8": "^2.0.5",
"axios": "^1.7.5",
"commander": "^12.1.0",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/Advertiser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { createHash } from 'node:crypto'
import { EventEmitter } from 'node:events'

import { getResponder, ServiceEvent, ServiceType } from '@homebridge/ciao'
import { systemBus } from '@homebridge/dbus-native'
import bonjour from 'bonjour-hap'
import createDebug from 'debug'

import { systemBus } from './dbus/index.js'
import { PromiseTimeout } from './util/promise-utils.js'

const debug = createDebug('HAP-NodeJS:Advertiser')
Expand Down
12 changes: 12 additions & 0 deletions src/lib/dbus/align.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Buffer } from 'safe-buffer'

export default function align(ps: any, n: number) {
const pad = n - (ps._offset % n)
if (pad === 0 || pad === n) {
return
}
// TODO: write8(0) in a loop (3 to 7 times here) could be more efficient
const padBuff = Buffer.alloc(pad)
ps.put(Buffer.from(padBuff))
ps._offset += pad
}
248 changes: 248 additions & 0 deletions src/lib/dbus/bus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// @ts-nocheck
import { EventEmitter } from 'node:events'

import constants from './constants.js'
import { introspectBus } from './introspect.js'
import stdDbusIfaces from './stdifaces.js'

export default function MessageBus(conn, opts) {
if (!(this instanceof MessageBus)) {
return new MessageBus(conn)
}
if (!opts) {
opts = {}
}

const self = this // eslint-disable-line ts/no-this-alias
this.connection = conn
this.serial = 1
this.cookies = {} // TODO: rename to methodReturnHandlers
this.methodCallHandlers = {}
this.signals = new EventEmitter()
this.exportedObjects = {}

this.invoke = function (msg, callback) {
if (!msg.type) {
msg.type = constants.messageType.methodCall
}
msg.serial = self.serial++
this.cookies[msg.serial] = callback
self.connection.message(msg)
}

this.invokeDbus = function (msg, callback) {
if (!msg.path) {
msg.path = '/org/freedesktop/DBus'
}
if (!msg.destination) {
msg.destination = 'org.freedesktop.DBus'
}
if (!msg.interface) {
msg.interface = 'org.freedesktop.DBus'
}
self.invoke(msg, callback)
}

this.mangle = function (path, iface, member) {
const obj = {}
if (typeof path === 'object') {
// handle one argument case mangle(msg)
obj.path = path.path
obj.interface = path.interface
obj.member = path.member
} else {
obj.path = path
obj.interface = iface
obj.member = member
}
return JSON.stringify(obj)
}

// Warning: errorName must respect the same rules as interface names (must contain a dot)
this.sendError = function (msg, errorName, errorText) {
const reply = {
type: constants.messageType.error,
serial: self.serial++,
replySerial: msg.serial,
destination: msg.sender,
errorName,
signature: 's',
body: [errorText],
}
this.connection.message(reply)
}

// route reply/error
this.connection.on('message', (msg) => {
function invoke(impl, func, resultSignature) {
Promise.resolve()
.then(() => {
return func.apply(impl, (msg.body || []).concat(msg))
})
.then(
(methodReturnResult) => {
const methodReturnReply = {
type: constants.messageType.methodReturn,
serial: self.serial++,
destination: msg.sender,
replySerial: msg.serial,
}
if (methodReturnResult !== null) {
methodReturnReply.signature = resultSignature
methodReturnReply.body = [methodReturnResult]
}
self.connection.message(methodReturnReply)
},
(e) => {
self.sendError(
msg,
e.dbusName || 'org.freedesktop.DBus.Error.Failed',
e.message || '',
)
},
)
}

let handler
if (msg.type === constants.messageType.methodReturn || msg.type === constants.messageType.error) {
handler = self.cookies[msg.replySerial]
if (handler) {
delete self.cookies[msg.replySerial]
const props = {
connection: self.connection,
bus: self,
message: msg,
signature: msg.signature,
}
let args = msg.body || []
if (msg.type === constants.messageType.methodReturn) {
args = [null].concat(args) // first argument - no errors, null
handler.apply(props, args) // body as array of arguments
} else {
handler.call(props, { name: msg.errorName, message: args }) // body as first argument
}
}
} else if (msg.type === constants.messageType.signal) {
self.signals.emit(self.mangle(msg), msg.body, msg.signature)
} else {
// methodCall
if (stdDbusIfaces(msg, self)) {
return
}

// exported interfaces handlers
const obj = self.exportedObjects[msg.path]
let iface

if (obj) {
iface = obj[msg.interface]
if (iface) {
// now we are ready to serve msg.member
const impl = iface[1]
const func = impl[msg.member]
if (!func) {
self.sendError(
msg,
'org.freedesktop.DBus.Error.UnknownMethod',
`Method "${msg.member}" on interface "${msg.interface}" doesn't exist`,
)
return
}
// TODO safety check here
const resultSignature = iface[0].methods[msg.member][1]
invoke(impl, func, resultSignature)
return
} else {
console.error(`Interface ${msg.interface} is not supported`)
// TODO: respond with standard dbus error
}
}

// setMethodCall handlers
handler = self.methodCallHandlers[self.mangle(msg)]
if (handler) {
invoke(null, handler[0], handler[1])
} else {
self.sendError(
msg,
'org.freedesktop.DBus.Error.UnknownService',
'Uh oh oh',
)
}
}
})

// register name
if (opts.direct !== true) {
this.invokeDbus({ member: 'Hello' }, (err, name) => {
if (err) {
throw new Error(err)
}
self.name = name
})
} else {
self.name = null
}

// eslint-disable-next-line unicorn/consistent-function-scoping
function DBusObject(name, service) {
this.name = name
this.service = service
this.as = function (name) {
return this.proxy[name]
}
}

function DBusService(name, bus) {
this.name = name
this.bus = bus
this.getObject = function (name, callback) {
if (name === undefined) {
return callback(new Error('Object name is null or undefined'))
}
const obj = new DBusObject(name, this)
introspectBus(obj, (err, ifaces, nodes) => {
if (err) {
return callback(err)
}
obj.proxy = ifaces
obj.nodes = nodes
callback(null, obj)
})
}

this.getInterface = function (objName, ifaceName, callback) {
this.getObject(objName, (err, obj) => {
if (err) {
return callback(err)
}
callback(null, obj.as(ifaceName))
})
}
}

this.getService = function (name) {
return new DBusService(name, this)
}

this.getObject = function (path, name, callback) {
const service = this.getService(path)
return service.getObject(name, callback)
}

this.getInterface = function (path, objname, name, callback) {
return this.getObject(path, objname, (err, obj) => {
if (err) {
return callback(err)
}
callback(null, obj.as(name))
})
}

this.removeMatch = function (match, callback) {
this.invokeDbus(
{ member: 'RemoveMatch', signature: 's', body: [match] },
callback,
)
}
}
Loading

0 comments on commit 2f8d567

Please sign in to comment.