UNPKG

dbus-sdk

Version:

A Node.js SDK for interacting with DBus, enabling seamless service calling and exposure with TypeScript support

288 lines (287 loc) 13.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LocalObject = void 0; const Errors_1 = require("./lib/Errors"); const IntrospectableInterface_1 = require("./lib/common/IntrospectableInterface"); const PropertiesInterface_1 = require("./lib/common/PropertiesInterface"); const PeerInterface_1 = require("./lib/common/PeerInterface"); /** * A class representing a local DBus object. * This class manages a collection of interfaces associated with a specific object path * within a local DBus service. It provides methods to add, remove, and query interfaces, * as well as to access standard DBus interfaces like Properties and Introspectable. * It serves as a container for interfaces under a unique object path. */ class LocalObject { /** * The name of this object, representing its DBus object path. * This uniquely identifies the object within a service (e.g., '/org/example/Object'). */ #name; /** * A map of interface names to their corresponding LocalInterface instances. * Stores all interfaces associated with this object for quick lookup and management. */ #interfaceMap = new Map(); /** * Getter for the DBus instance associated with this object's service. * Provides access to the DBus connection for operations like signal emission. * * @returns The DBus instance if the service is defined, otherwise undefined. */ get dbus() { if (!this.service) return; return this.service.dbus; } /** * Getter for the name (object path) of this local object. * Returns the validated object path set during construction. * * @returns The object path as a string (e.g., '/org/example/Object'). */ get name() { return this.#name; } /** * Getter for the Properties interface associated with this object. * Provides access to the standard 'org.freedesktop.DBus.Properties' interface * for handling property-related operations across all interfaces on this object. * * @returns The PropertiesInterface instance for handling property-related operations. */ get propertiesInterface() { return this.findInterfaceByName('org.freedesktop.DBus.Properties'); } /** * Getter for the Introspectable interface associated with this object. * Provides access to the standard 'org.freedesktop.DBus.Introspectable' interface * for handling introspection operations to describe the object's structure. * * @returns The IntrospectableInterface instance for handling introspection operations. */ get introspectableInterface() { return this.findInterfaceByName('org.freedesktop.DBus.Introspectable'); } /** * Getter for the Peer interface associated with this object. * Provides access to the standard 'org.freedesktop.DBus.Peer' interface * for handling peer-related operations like ping and machine ID retrieval. * * @returns The PeerInterface instance for handling peer-related operations. */ get peerInterface() { return this.findInterfaceByName('org.freedesktop.DBus.Peer'); } /** * Constructor for LocalObject. * Initializes the object with a validated object path and adds standard DBus interfaces * (Properties, Introspectable, and Peer) for compliance with DBus conventions. * * @param objectPath - The DBus object path to be validated and set (e.g., '/org/example/Object'). * @throws {LocalObjectInvalidNameError} If the provided object path does not meet DBus naming criteria. */ constructor(objectPath) { this.#name = this.validateDBusObjectPath(objectPath); // Add standard DBus interfaces required for most objects this.addInterface(new PropertiesInterface_1.PropertiesInterface()); this.addInterface(new IntrospectableInterface_1.IntrospectableInterface()); this.addInterface(new PeerInterface_1.PeerInterface()); } /** * Validates a DBus object path based on DBus naming rules. * Ensures the path is a non-empty string, within length limits, starts with a slash, * does not end with a slash (except for root path '/'), avoids consecutive slashes, * and uses only allowed characters (letters, digits, underscores) in each element. * * @param objectPath - The path to validate. * @returns The validated object path if it passes all checks. * @throws {LocalObjectInvalidNameError} If the path does not meet DBus naming criteria. */ validateDBusObjectPath(objectPath) { // Step 1: Check if the input is a string and not empty if (typeof objectPath !== 'string' || objectPath.length === 0) { throw new Errors_1.LocalObjectInvalidNameError('Object path must be a non-empty string.'); } // Step 2: Check length limit (maximum 255 bytes, consistent with bus name limit) if (objectPath.length > 255) { throw new Errors_1.LocalObjectInvalidNameError('Object path exceeds 255 bytes.'); } // Step 3: Check if it starts with a slash if (!objectPath.startsWith('/')) { throw new Errors_1.LocalObjectInvalidNameError('Object path must start with a slash (/).'); } // Step 4: Special case: root path "/" if (objectPath === '/') { return objectPath; } // Step 5: Check if it ends with a slash (disallowed except for root path) if (objectPath.endsWith('/')) { throw new Errors_1.LocalObjectInvalidNameError('Object path cannot end with a slash (except for root path /).'); } // Step 6: Check for consecutive slashes if (objectPath.includes('//')) { throw new Errors_1.LocalObjectInvalidNameError('Object path cannot contain consecutive slashes (//).'); } // Step 7: Split the object path into elements (remove leading slash first) const elements = objectPath.slice(1).split('/'); // Step 8: Validate each element for (let i = 0; i < elements.length; i++) { const element = elements[i]; // Check if element is empty (should not happen after previous checks, but for safety) if (element.length === 0) { throw new Errors_1.LocalObjectInvalidNameError(`Element at position ${i + 1} is empty.`); } // Check if element starts with a digit if (element.match(/^[0-9]/)) { throw new Errors_1.LocalObjectInvalidNameError(`Element "${element}" at position ${i + 1} cannot start with a digit.`); } // Check if element contains only allowed characters (letters, digits, underscore) for (let j = 0; j < element.length; j++) { const char = element[j]; if (!/[a-zA-Z0-9_]/.test(char)) { throw new Errors_1.LocalObjectInvalidNameError(`Element "${element}" at position ${i + 1} contains invalid character "${char}".`); } } } // All checks passed, return the object path return objectPath; } /** * Sets the LocalService associated with this object. * Links the object to a specific service within a DBus connection for context during operations. * * @param service - The LocalService to associate with this object, or undefined to clear the association. * @returns The instance of this LocalObject for method chaining. */ setService(service) { this.service = service; return this; } /** * Getter for the introspection data of this object. * Provides metadata about all interfaces associated with this object for DBus introspection. * * @returns An IntrospectNode object containing the introspection data for all interfaces associated with this object. */ get introspectNode() { const interfaces = []; this.#interfaceMap.forEach((localInterface) => { interfaces.push(localInterface.introspectInterface); }); return { interface: interfaces }; } /** * Adds a LocalInterface to this object. * Associates the interface with this object, linking it to the object's context, * and notifies the service's object manager of the addition if applicable. * * @param localInterface - The LocalInterface instance to add to this object. * @returns A boolean indicating whether the interface was successfully added (true if added, false if already present). * @throws {LocalInterfaceExistsError} If an interface with the same name already exists and is not the same instance. */ addInterface(localInterface) { let addSuccess = false; if (this.#interfaceMap.has(localInterface.name)) { if (this.#interfaceMap.get(localInterface.name) !== localInterface) { throw new Errors_1.LocalInterfaceExistsError(`Local interface ${localInterface.name} exists`); } else { return addSuccess; // Interface already exists and is the same instance, no action needed } } localInterface.setObject(this); // Link the interface to this object this.#interfaceMap.set(localInterface.name, localInterface); addSuccess = true; if (addSuccess) { const addedInterfaceRecord = {}; // Fetch managed properties and notify the object manager addedInterfaceRecord[localInterface.name] = localInterface.getManagedProperties(); this.service?.objectManager?.interfacesAdded(this, addedInterfaceRecord); } return addSuccess; } /** * Removes a LocalInterface from this object by name or instance. * This method handles both string (interface name) and LocalInterface instance as input, * unlinking the interface and notifying the object manager of the removal. * * @param inp - The name of the interface or the LocalInterface instance to remove. * @returns A boolean indicating whether the interface was successfully removed (true if removed, false if not found). */ removeInterface(inp) { let removeSuccess; let removedInterface; if (typeof inp === 'string') { // Case 1: Input is a string representing the interface name. // Attempts to find and unset the associated object before deleting the interface. this.#interfaceMap.get(inp)?.setObject(undefined); removedInterface = this.#interfaceMap.get(inp); removeSuccess = this.#interfaceMap.delete(inp); } else { // Case 2: Input is a LocalInterface instance. // Finds the interface by instance, unsets the associated object, and deletes it. const result = [...this.#interfaceMap.entries()].find(([interfaceName, localInterface]) => localInterface === inp); if (!result) { removeSuccess = false; } else { result[1].setObject(undefined); removedInterface = result[1]; removeSuccess = this.#interfaceMap.delete(result[0]); } } // If removal was successful, notify the object manager of the removed interface if (removedInterface && removeSuccess) this.service?.objectManager?.interfacesRemoved(this, [removedInterface.name]); return removeSuccess; } /** * Lists all interfaces associated with this object. * Provides a convenient way to inspect all interfaces currently linked to the object. * * @returns A record mapping interface names to their LocalInterface instances. */ listInterfaces() { const interfaces = {}; this.#interfaceMap.forEach((localInterface, interfaceName) => interfaces[interfaceName] = localInterface); return interfaces; } /** * Lists the names of all interfaces associated with this object. * Provides a quick way to retrieve just the names of the interfaces for enumeration. * * @returns An array of interface names as strings. */ interfaceNames() { return [...this.#interfaceMap.keys()]; } /** * Finds a LocalInterface by its name. * Allows retrieval of a specific interface with type casting for specialized interface types. * * @param name - The name of the interface to find (e.g., 'org.example.MyInterface'). * @returns The LocalInterface instance of the specified type if found, otherwise undefined. * @template T - The type of LocalInterface to cast the result to (defaults to LocalInterface). */ findInterfaceByName(name) { return this.#interfaceMap.get(name); } /** * Gets all managed interfaces and their properties as a record. * Retrieves the current properties of all interfaces on this object as DBusSignedValue instances. * * @returns A record mapping interface names to their property records (property name to DBusSignedValue). */ getManagedInterfaces() { const record = {}; for (const interfaceName of this.interfaceNames()) { record[interfaceName] = this.#interfaceMap.get(interfaceName).getManagedProperties(); } return record; } } exports.LocalObject = LocalObject;