diff --git a/dm-framework/datamodels/Makefile b/dm-framework/datamodels/Makefile index 5b631911a..cce05226c 100644 --- a/dm-framework/datamodels/Makefile +++ b/dm-framework/datamodels/Makefile @@ -34,8 +34,9 @@ define Build/Prepare mkdir -p $(PKG_BUILD_DIR) $(CP) -rf ./src/* $(PKG_BUILD_DIR)/ cd $(PKG_BUILD_DIR); \ - npm install better-sqlite3 && \ - node json2code.js + npm install better-sqlite3 quickjs && \ + node ./scripts/json2code.js && \ + node ./scripts/qjs-handlers-validate.js endef TARGET_CFLAGS += $(FPIC) -I$(PKG_BUILD_DIR) @@ -62,12 +63,13 @@ endef define Package/$(PKG_NAME)/install $(INSTALL_DIR) $(1)/usr/lib $(INSTALL_DIR) $(1)/etc/config - $(INSTALL_DIR) $(1)/usr/lib/quickjs - $(INSTALL_DIR) $(1)/usr/lib/quickjs/dm_handlers - cd $(PKG_BUILD_DIR)/dm-files && find . -type d -exec mkdir -p $(1)/usr/lib/quickjs/dm_handlers/{} \; - cd $(PKG_BUILD_DIR)/dm-files && find . -name "*.js" -not -name ".*.js" -not -name "makeDM.js" -type f -exec cp {} $(1)/usr/lib/quickjs/dm_handlers/{} \; + $(INSTALL_DIR) $(1)/usr/lib/dmf_handlers $(INSTALL_BIN) $(PKG_BUILD_DIR)/default.db $(1)/etc/default_dm.db $(INSTALL_BIN) $(PKG_BUILD_DIR)/libdm.so $(1)/usr/lib/ + # Copy only .js handler files recursively, preserving folder structure (skip hidden files/folders) + ( cd $(PKG_BUILD_DIR)/dm-files; \ + find . -type d -not -path './.*' -exec $(INSTALL_DIR) $(1)/usr/lib/dmf_handlers/{} \; ; \ + find . -type f -name '*.js' -not -path './.*' -exec $(INSTALL_BIN) {} $(1)/usr/lib/dmf_handlers/{} \; ) endef $(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/dm-framework/datamodels/src/._qjs-handlers-validate.js b/dm-framework/datamodels/src/._qjs-handlers-validate.js new file mode 100755 index 000000000..afceea39b Binary files /dev/null and b/dm-framework/datamodels/src/._qjs-handlers-validate.js differ diff --git a/dm-framework/datamodels/src/dm-files/Bridge/bridge-apply.js b/dm-framework/datamodels/src/dm-files/Bridge/bridge-apply.js index 67570575b..b5fe8ddea 100644 --- a/dm-framework/datamodels/src/dm-files/Bridge/bridge-apply.js +++ b/dm-framework/datamodels/src/dm-files/Bridge/bridge-apply.js @@ -1,6 +1,5 @@ -/* eslint-disable no-underscore-dangle */ /* - * Copyright (c) 2023 Genexis B.V. All rights reserved. + * Copyright (c) 2025 Genexis B.V. All rights reserved. * * This Software and its content are protected by the Dutch Copyright Act * ('Auteurswet'). All and any copying and distribution of the software @@ -14,15 +13,12 @@ import { getUciOption, getUciByType, setUci, addUci, delUci, delUciOption, } from '../uci.js'; import * as dm from '../dm_consts.js'; -import { getBridgeInterfaceName } from './bridge.js'; function clearUnusedDevice(oldPorts, newPorts, devices) { oldPorts?.forEach((port) => { if (port.includes('.') && !newPorts?.includes(port)) { const dev = devices?.find((x) => x.name === port); - if (dev) { - delUci('network', dev['.name']); - } + dev?.delUci('network', dev['.name']); } }); } @@ -41,7 +37,11 @@ function applyBridge(bri, ports, VLANs, VLANPorts) { continue; } - let ifname = _dm_get(`${port.LowerLayers}.Name`); + let ifname = _dm_linker_value(port.LowerLayers); + if (!ifname) { + _log_error(`ifname not found for port: ${port.LowerLayers}`); + continue; + } // check vlan const portPath = `Device.Bridging.Bridge.${bri['.index']}.Port.${port['.index']}`; const vp = VLANPorts?.find((x) => x.Port === portPath); @@ -103,25 +103,6 @@ function applyBridge(bri, ports, VLANs, VLANPorts) { if (ifnames.length > 0) { setUci('network', bri._key, { ports: ifnames }); } - - // apply for the WiFi.SSID port - const ssids = _dm_get('Device.WiFi.SSID.'); - if (ssids && ssids.length > 0) { - const networkName = getBridgeInterfaceName(bri['.index']); - if (!networkName) { - _log_error('network name is not found for bridge'); - return; - } - const ssidPorts = ports?.filter((x) => x.LowerLayers.includes('WiFi.SSID.')); - ssids?.forEach((ssid) => { - const path = `Device.WiFi.SSID.${ssid['.index']}`; - if (ssidPorts?.find((p) => p.Enable && p.LowerLayers === path)) { - setUci('wireless', ssid._key, { network: networkName }); - } else if (ssid.network === networkName) { - delUciOption('wireless', ssid._key, 'network'); - } - }); - } } export function applyDeviceBridgingBridgePort(ports, bri) { @@ -148,8 +129,7 @@ export function initDeviceBridgingBridge(bri) { name: bri.Name, enabled: '0', }); - // create empty interface for the bridge, otherwise the bridge will not be up. - // the empty interface will be deleted if there is a real interface is created later. + // create empty interface for the bridge addUci('network', 'interface', `itf_${bri._key}`, { device: bri.Name, layer2_bridge: '1', diff --git a/dm-framework/datamodels/src/dm-files/Bridge/bridge-import.js b/dm-framework/datamodels/src/dm-files/Bridge/bridge-import.js index f5a2c1384..e1598d084 100644 --- a/dm-framework/datamodels/src/dm-files/Bridge/bridge-import.js +++ b/dm-framework/datamodels/src/dm-files/Bridge/bridge-import.js @@ -93,50 +93,6 @@ function importBridge(dev, devices, bridges) { } } - // // wait until wifi config is available - // const hasWifi = runCommand('db get hw.WIFI.start') !== undefined; - // if (hasWifi) { - // _log_info('waiting for wifi config'); - // const hasWifiConfig = waitUntilFileExists('/etc/config/wireless', 10000); - - // _log_info(`wifi config found : ${hasWifiConfig}`); - - // let wifiIfaces = []; - // const startTime = Date.now(); - // while ((!wifiIfaces || wifiIfaces.length === 0) && (Date.now() - startTime < 30000)) { - // wifiIfaces = getUciByType('wireless', 'wifi-iface'); - // if (!wifiIfaces || wifiIfaces.length === 0) { - // _log_info('no wifi ifaces found, waiting for 1 second'); - // os.sleep(1000); - // } - // } - - // _log_info(`wifiIfaces: \n${JSON.stringify(wifiIfaces, null, 2)}`); - // wifiIfaces = wifiIfaces?.filter((x) => !(x.multi_ap === '1' || x.type === 'backhaul')); - // if (wifiIfaces && wifiIfaces.length > 0) { - // wifiIfaces?.forEach((x, i) => { - // x.index = i + 1; - // }); - // const itfSect = getUciByType('network', 'interface', { match: { device: dev.name } }); - // if (itfSect && itfSect.length > 0) { - // const itfName = itfSect[0]['.name']; - // wifiIfaces = wifiIfaces.filter((x) => x.network === itfName); - // wifiIfaces.forEach((x) => { - // briPorts.push({ - // Enable: 1, - // Name: x.ifname ? x.ifname : x['.name'], - // Alias: `cpe-${x['.name']}`, - // TPID: 37120, - // PVID: 1, - // Type: 'CustomerVLANPort', - // LowerLayers: `Device.WiFi.SSID.${x.index}`, - // _key: x['.name'], - // }); - // }); - // } - // } - // } - if (briPorts.length > 1) { const indexes = Array.from({ length: briPorts.length - 1 }, (v, i) => i + 2); briPorts[0].LowerLayers = indexes.map((i) => `Device.Bridging.Bridge.${bridges.length}.Port.${i}`).join(','); @@ -154,5 +110,3 @@ export function importDeviceBridgingBridge() { return bridges; } - -export default importDeviceBridgingBridge; diff --git a/dm-framework/datamodels/src/dm-files/Bridge/bridge.js b/dm-framework/datamodels/src/dm-files/Bridge/bridge.js index 20f7a0ebf..9798e0665 100644 --- a/dm-framework/datamodels/src/dm-files/Bridge/bridge.js +++ b/dm-framework/datamodels/src/dm-files/Bridge/bridge.js @@ -11,7 +11,7 @@ import * as std from 'std'; import { getUciByType } from '../uci.js'; -import { findPathInLowerlayer } from '../utils.js'; +import * as uci2 from '../uci2.js'; function setMgmtPortLowerLayers(bri) { if (!bri) { @@ -66,7 +66,6 @@ export function getDeviceBridgingBridgePortStatus(bri, port) { } export function infoDeviceBridgingBridgePort(path, port) { - _log_info(`infoDeviceBridgingBridgePort: ${path} =>:\n ${JSON.stringify(port, null, 2)}`); const mgmtPort = _dm_get(`${path}.ManagementPort`); if (typeof mgmtPort === 'undefined' || mgmtPort) { return; @@ -80,7 +79,6 @@ export function infoDeviceBridgingBridgePort(path, port) { } export function getDeviceBridgingBridgePortStatsBytesSent(bri, port) { - _log_info(`getDeviceBridgingBridgePortStatsBytesSent: ${port.ifname}`); return std.loadFile(`/sys/class/net/${port.ifname}/statistics/tx_bytes`)?.trim(); } @@ -140,38 +138,42 @@ export function getDeviceBridgingBridgePortStatsUnknownProtoPacketsReceived(bri, return std.loadFile(`/sys/class/net/${port.ifname}/statistics/rx_unknown_packets`)?.trim(); } -export function getBridgeInterfaceName(briIndex) { - const insts = _dm_instances('Device.IP.Interface.'); - const inst = insts.find((x) => findPathInLowerlayer(x, `Device.Bridging.Bridge.${briIndex}.Port.1`, 'Bridging.Bridge.')); - if (inst) { - return _dm_get(`${inst}._key`); - } - return undefined; -} - export function getDeviceBridgingBridgePort(bri) { let networkName = bri.Name; if (bri.Name.startsWith('br-')) { networkName = bri.Name.slice(3); } - // add Port for WiFi SSIDs - const wifiIfaces = getUciByType('wireless', 'wifi-iface', { match: { multi_ap: '1' } }); + + const wifiIfaces = getUciByType('wireless', 'wifi-iface', { match: { multi_ap: '2' } }); wifiIfaces?.forEach((x, i) => { - x.index = i + 1; + const ssid = uci2.getUciByType('dmmap_wireless', 'ssid', + { match: { device: x.device, ssid: x.ssid}, confdir: '/etc/bbfdm/dmmap'}); + x.ssidPath = _dm_linker_path("Device.WiFi.SSID.", "Name", ssid[0]?.name) ?? ''; }); - _log_info(`getDeviceBridgingBridgePort: ${networkName} =>:\n ${JSON.stringify(wifiIfaces, null, 2)}`); return wifiIfaces?.filter((x) => x.network === networkName); } export function getDeviceBridgingBridgePortLowerLayers(bri, port) { - return `Device.WiFi.SSID.${port.index}`; + return port.ssidPath; } export function getDeviceBridgingBridgePortManagementPort() { return '0'; } +export function getDeviceBridgingBridgePortPVID() { + return '1'; +} + +export function getDeviceBridgingBridgePortTPID() { + return '37120'; +} + +export function getDeviceBridgingBridgePortType() { + return 'CustomerVLANPort'; +} + export function getDeviceBridgingBridgePortName(bri, port) { return port.ifname; } diff --git a/dm-framework/datamodels/src/dm-files/uci2.js b/dm-framework/datamodels/src/dm-files/uci2.js new file mode 100755 index 000000000..548e94dad --- /dev/null +++ b/dm-framework/datamodels/src/dm-files/uci2.js @@ -0,0 +1,126 @@ + +/* eslint-disable no-undef */ +/* + * Wrapper around the native QuickJS C binding `_uci_call` which speaks to + * libuci directly (see qjs_uci_api.c). The exported helpers mimic the public + * API of the original uci.js module so that existing call-sites can switch to + * this implementation by simply importing uci2.js. + */ + +export function uciBool(val) { + // by default enable is true if it is not defined + return (val === undefined || val === '1' || val === 'true' || val === 'enable' || val === 'yes'); +} + +function callUci(method, args) { + const [ret, res] = _uci_call(method, args); + if (ret !== 0) { + // Returning undefined on error keeps behaviour consistent with the + // original helpers which silently return undefined. + return [ret, undefined]; + } + return [ret, res]; +} + +export function getUci(args) { + const [, res] = callUci('get', args); + if (res) { + if (res.values) { + if (!args.section) { + return Object.values(res.values); + } + return res.values; + } + if (res.value !== undefined) { + return res.value; + } + } + return undefined; +} + +export function getUciOption(config, section, option, extraArgs) { + let args = { config, section, option }; + if (extraArgs) { + args = { ...args, ...extraArgs }; + } + return getUci(args); +} + +export function getUciByType(config, type, extraArgs) { + let args = { config, type }; + if (extraArgs) { + args = { ...args, ...extraArgs }; + } + return getUci(args); +} + +export function getUciSection(config, section, extraArgs) { + let args = { config, section }; + if (extraArgs) { + args = { ...args, ...extraArgs }; + } + return getUci(args); +} + +export function setUci(cfg, section, values, type, match, extraArgs) { + let args = { config: cfg, section }; + if (type) args.type = type; + if (values) args.values = values; + if (match) args.match = match; + if (extraArgs) args = { ...args, ...extraArgs }; + + const [ret] = callUci('set', args); + return ret; +} + +export function addUci(cfg, type, name, values, extraArgs) { + let args = { config: cfg, type }; + if (name) args.name = name; + if (values) args.values = values; + if (extraArgs) args = { ...args, ...extraArgs }; + + const [, res] = callUci('add', args); + return res || undefined; +} + +export function delUci(cfg, section, type, option, options, match, extraArgs) { + let args = { config: cfg }; + if (section) args.section = section; + if (type) args.type = type; + if (option) args.option = option; + if (options) args.options = options; + if (match) args.match = match; + if (extraArgs) args = { ...args, ...extraArgs }; + + const [, res] = callUci('delete', args); + return res || undefined; +} + +export function delUciOption(config, section, option, match, extraArgs) { + let args = { config, section, option }; + if (match) args.match = match; + if (extraArgs) args = { ...args, ...extraArgs }; + const [, res] = callUci('delete', args); + return res || undefined; +} + +export function uciChanges(cfg, extraArgs) { + let args = { config: cfg }; + if (extraArgs) args = { ...args, ...extraArgs }; + const [, res] = callUci('changes', args); + return res && res.changes ? res.changes : undefined; +} + +export function commitUci(cfg, extraArgs) { + let args = { config: cfg }; + if (extraArgs) args = { ...args, ...extraArgs }; + const [ret] = callUci('commit', args); + return ret; +} + +export function revertUci(cfg, extraArgs) { + let args = { config: cfg }; + if (extraArgs) args = { ...args, ...extraArgs }; + const [ret] = callUci('revert', args); + return ret; +} \ No newline at end of file diff --git a/dm-framework/datamodels/src/scripts/._json2code.js b/dm-framework/datamodels/src/scripts/._json2code.js new file mode 100755 index 000000000..afceea39b Binary files /dev/null and b/dm-framework/datamodels/src/scripts/._json2code.js differ diff --git a/dm-framework/datamodels/src/scripts/._qjs-handlers-validate.js b/dm-framework/datamodels/src/scripts/._qjs-handlers-validate.js new file mode 100755 index 000000000..afceea39b Binary files /dev/null and b/dm-framework/datamodels/src/scripts/._qjs-handlers-validate.js differ diff --git a/dm-framework/datamodels/src/json2code.js b/dm-framework/datamodels/src/scripts/json2code.js similarity index 98% rename from dm-framework/datamodels/src/json2code.js rename to dm-framework/datamodels/src/scripts/json2code.js index c85cb81fa..f7313cce7 100755 --- a/dm-framework/datamodels/src/json2code.js +++ b/dm-framework/datamodels/src/scripts/json2code.js @@ -17,7 +17,16 @@ const path = require('path'); const fsSync = require('fs').promises; const sqlite3 = require('better-sqlite3'); -const folderPath = './dm-files'; +// ------------------------------------------------------------ +// File path constants +// ------------------------------------------------------------ +const DM_FILES_PATH = 'dm-files'; +const DM_HEADER_FILE = 'dm.h'; +const DM_CONSTS_JS_FILE = `${DM_FILES_PATH}/dm_consts.js`; +const DM_C_FILE = `dm.c`; +const DEFAULT_DB_FILE = `default.db`; +const EXPORTS_JS_FILE = `exports.js`; + // ------------------------------------------------------------ // Vendor prefix handling // ------------------------------------------------------------ @@ -825,7 +834,7 @@ async function exportJSNodeID(file, obj) { } async function exportDMHeaderFile() { - const headerFile = 'dm.h'; + const headerFile = DM_HEADER_FILE; nodeID = 0; await fsSync.writeFile(headerFile, license); @@ -843,7 +852,7 @@ async function exportDMHeaderFile() { } async function exportJSHeaderFile() { - const file = './dm-files/dm_consts.js'; + const file = DM_CONSTS_JS_FILE; nodeID = 0; await fsSync.writeFile(file, license); @@ -854,7 +863,7 @@ async function exportJSHeaderFile() { } async function exportDMCFile() { - const cfile = 'dm.c'; + const cfile = DM_C_FILE; await fsSync.writeFile(cfile, license); await fsSync.appendFile(cfile, head); await dumpNodeDeclear(cfile, rootObj); @@ -1053,7 +1062,7 @@ function createDynamicTables(obj, db) { } function createSqliteDB() { - const dbFile = 'default.db'; + const dbFile = DEFAULT_DB_FILE; if (fs.existsSync(dbFile)) { fs.unlinkSync(dbFile); } @@ -1156,12 +1165,12 @@ function createExportFile(directory) { }); const headerComment = `// This file is auto-generated. Do not modify it directly!\n\n`; - fs.writeFileSync(path.join(directory, 'exports.js'), headerComment + exportStatements); + fs.writeFileSync(path.join(directory, EXPORTS_JS_FILE), headerComment + exportStatements); } (async () => { try { - const objects = combineJSONFiles('./dm-files'); + const objects = combineJSONFiles(DM_FILES_PATH); // Resolve any {BBF_VENDOR_PREFIX} placeholders BEFORE running other helpers objects.forEach((o) => deepReplaceVendorPrefix(o)); convertPathRef(objects); @@ -1200,7 +1209,7 @@ function createExportFile(directory) { await exportDMCFile(); await exportDMHeaderFile(); await exportJSHeaderFile(); - createExportFile('./dm-files'); + createExportFile(DM_FILES_PATH); createSqliteDB(); console.log('done.'); } catch (error) { diff --git a/dm-framework/datamodels/src/dm-files/makeDM.js b/dm-framework/datamodels/src/scripts/makeDM.js similarity index 100% rename from dm-framework/datamodels/src/dm-files/makeDM.js rename to dm-framework/datamodels/src/scripts/makeDM.js diff --git a/dm-framework/datamodels/src/scripts/qjs-handlers-validate.js b/dm-framework/datamodels/src/scripts/qjs-handlers-validate.js new file mode 100644 index 000000000..e0a094529 --- /dev/null +++ b/dm-framework/datamodels/src/scripts/qjs-handlers-validate.js @@ -0,0 +1,88 @@ +// This script is used to load and validate the js handlers code in dm-file. +(function () { + const fs = require('fs'); + const path = require('path'); + const { spawnSync } = require('child_process'); + + // Root directory (dm-files) relative to this script + const dmFilesRoot = path.resolve(__dirname, '../dm-files'); + + /** + * Recursively walk a directory and collect all *.js files that do not start with a dot. + * @param {string} dir - directory to walk + * @param {string[]} out - accumulator for file paths + */ + function collectJsFiles(dir, out) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + // Skip hidden files/directories (starting with ".") + if (entry.name.startsWith('.')) { + continue; + } + + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + collectJsFiles(fullPath, out); + } else if (entry.isFile() && fullPath.endsWith('.js')) { + out.push(fullPath); + } + } + } + + /** + * Validate a single JavaScript file with QuickJS (qjs). + * Exits the process with -1 on failure. + * @param {string} filePath - absolute path of the JS file + */ + function validateWithQjs(filePath) { + // Extract directory and filename for proper working directory + const fileDir = path.dirname(filePath); + const fileName = path.basename(filePath); + + // Capture stdout/stderr so we can print them on failure + // Set the working directory to the file's directory + const result = spawnSync('qjs', [fileName], { + encoding: 'utf8', + cwd: fileDir + }); + + if (result.status === 0) { + return; // Validated successfully + } + + // Show QuickJS output so user sees error details + console.error(`\n===== QuickJS validation failed: ${filePath} =====`); + if (result.stdout) { + console.error(result.stdout.trim()); + } + if (result.stderr) { + console.error(result.stderr.trim()); + } + console.error('==============================================='); + process.exit(-1); + } + + function main() { + if (!fs.existsSync(dmFilesRoot)) { + console.error(`dm-files directory not found at: ${dmFilesRoot}`); + process.exit(-1); + } + + const jsFiles = []; + collectJsFiles(dmFilesRoot, jsFiles); + + if (jsFiles.length === 0) { + console.log('No JavaScript files found to validate.'); + return; + } + + console.log(`Validating ${jsFiles.length} JavaScript file(s) with QuickJS...`); + jsFiles.forEach(validateWithQjs); + console.log('All files validated successfully.'); + } + + // Execute when run directly (not required when imported) + if (require.main === module) { + main(); + } +})(); \ No newline at end of file diff --git a/dm-framework/datamodels/src/dm-files/tr-181-2-19-1-cwmp-full.xml b/dm-framework/datamodels/src/scripts/tr-181-2-19-1-cwmp-full.xml similarity index 100% rename from dm-framework/datamodels/src/dm-files/tr-181-2-19-1-cwmp-full.xml rename to dm-framework/datamodels/src/scripts/tr-181-2-19-1-cwmp-full.xml diff --git a/dm-framework/datamodels/src/dm-files/tr-181-2-19-1-usp-full.xml b/dm-framework/datamodels/src/scripts/tr-181-2-19-1-usp-full.xml similarity index 100% rename from dm-framework/datamodels/src/dm-files/tr-181-2-19-1-usp-full.xml rename to dm-framework/datamodels/src/scripts/tr-181-2-19-1-usp-full.xml diff --git a/dm-framework/datamodels/src/dm-files/tr181-full-objects.json b/dm-framework/datamodels/src/scripts/tr181-full-objects.json similarity index 100% rename from dm-framework/datamodels/src/dm-files/tr181-full-objects.json rename to dm-framework/datamodels/src/scripts/tr181-full-objects.json diff --git a/dm-framework/dm-agent/files/etc/init.d/bridging b/dm-framework/dm-agent/files/etc/init.d/bridging index 37bb2cd29..d09542a6e 100755 --- a/dm-framework/dm-agent/files/etc/init.d/bridging +++ b/dm-framework/dm-agent/files/etc/init.d/bridging @@ -77,6 +77,7 @@ handle_ebtables_rule() { } start_service() { + ubus -t 30 wait_for network.device uci config_load bridging config_foreach handle_ebtables_chain chain config_foreach handle_ebtables_rule rule diff --git a/dm-framework/dm-agent/files/etc/init.d/dm-agent b/dm-framework/dm-agent/files/etc/init.d/dm-agent deleted file mode 100644 index 60ada6ee6..000000000 --- a/dm-framework/dm-agent/files/etc/init.d/dm-agent +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/sh /etc/rc.common - -START=99 -STOP=10 - -USE_PROCD=1 -PROG=/usr/sbin/dm-agent - -start_service() { - logger -t dmapi "waiting network ubus" - ubus -t 30 wait_for network.device uci - [ "$?" -eq 0 ] && { - logger -t dmapi "waiting for network ubus done" - } || { - logger -t dmapi "failed to wait for network ubus" - } - - [ -f "/etc/config/wireless" ] && { - logger -t dmapi "waiting wifi ubus" - ubus -t 30 wait_for wifi - [ "$?" -eq 0 ] && { - logger -t dmapi "waiting for wifi ubus done" - } || { - logger -t dmapi "failed to wait for wifi ubus" - } - } - - procd_open_instance dm-agent - procd_set_param command ${PROG} - procd_set_param respawn - procd_close_instance -} - -reload_service() { - stop - start -} diff --git a/dm-framework/dm-api/src/Makefile b/dm-framework/dm-api/src/Makefile index 670695db7..542686195 100644 --- a/dm-framework/dm-api/src/Makefile +++ b/dm-framework/dm-api/src/Makefile @@ -26,6 +26,7 @@ SRCS = \ quickjs/qjs.c \ quickjs/qjs_log.c \ quickjs/qjs_dm_api.c \ + quickjs/qjs_uci_api.c \ quickjs/qjs_ubus_api.c OBJS = $(SRCS:.c=.o) diff --git a/dm-framework/dm-api/src/core/dm_api.c b/dm-framework/dm-api/src/core/dm_api.c index 67c396eed..070d755fc 100644 --- a/dm-framework/dm-api/src/core/dm_api.c +++ b/dm-framework/dm-api/src/core/dm_api.c @@ -93,7 +93,7 @@ int dmapi_init(const char *service_name) global_ctx.session_state = SESSION_STATE_IDLE; strncpy(global_ctx.service_name, service_name, sizeof(global_ctx.service_name)); - dmlog_init("dmapi", 0); + dmlog_init("dmf", 0); dmlog_debug("dmapi_init starting, service_name: %s", global_ctx.service_name); if (qjs_init() != 0) { diff --git a/dm-framework/dm-api/src/core/dm_linker.c b/dm-framework/dm-api/src/core/dm_linker.c index 10ca98543..a2a2073bc 100755 --- a/dm-framework/dm-api/src/core/dm_linker.c +++ b/dm-framework/dm-api/src/core/dm_linker.c @@ -514,3 +514,105 @@ int dm_resolve_linker(const char *object_path, char **value) { return 0; } +/** + * Resolve the object path for a given linker value. + * + * This helper reconstructs the deterministic linker string of the form + * "[==]." + * that was generated by dm_refresh_linker_nodes() and uses its hash to + * look up the real object path from the reference database. + * + * If no matching entry exists in the database the function will fall + * back to returning the linker string itself, exactly mimicking the + * behaviour of legacy _bbfdm_get_references(). + * + * The caller is responsible for freeing the returned string. + * + * @param base_path Base object path ending with a dot (e.g. "Device.WiFi.SSID.") + * @param key_name Parameter name that acts as linker key (e.g. "Name") + * @param key_value Desired value of the linker key + * @param object_path [out] malloc-allocated string holding either the + * resolved object path (e.g. "Device.WiFi.SSID.4") + * (NULL when no match is found) + * @return 0 when resolved, -1 if no match/error + */ +int dm_resolve_linker_path(const char *base_path, + const char *key_name, + const char *key_value, + char **object_path) { + if (!base_path || !key_name || !key_value || !object_path) { + dmlog_error("Invalid arguments to dm_resolve_linker_path"); + return -1; + } + + if (strlen(base_path) == 0 || strlen(key_name) == 0 || strlen(key_value) == 0) { + dmlog_error("base_path, key_name and key_value must not be empty"); + return -1; + } + + *object_path = NULL; + + /* Compose the linker string that was used as basis for the reference hash */ + size_t linker_len = strlen(base_path) + strlen(key_name) + strlen(key_value) + 6; /* "[==]." */ + char *linker_string = malloc(linker_len); + if (!linker_string) { + return -1; + } + snprintf(linker_string, linker_len, "%s[%s==%s].", base_path, key_name, key_value); + + /* Calculate hash for use as option name inside reference_path section */ + char *hash_path = calculate_hash(linker_string); + if (!hash_path) { + free(linker_string); + return -1; + } + + /* Open UCI context pointing to runtime reference DB (/var/state) */ + struct uci_context *ctx = uci_alloc_context(); + if (!ctx) { + dmlog_error("Failed to allocate UCI context"); + free(linker_string); + free(hash_path); + return -1; + } + uci_set_confdir(ctx, "/var/state"); + + struct uci_package *pkg = NULL; + if (uci_load(ctx, UCI_PACKAGE, &pkg) != UCI_OK || !pkg) { + dmlog_error("Failed to load UCI package %s", UCI_PACKAGE); + uci_free_context(ctx); + free(linker_string); + free(hash_path); + return -1; + } + + struct uci_section *ref_path_section = uci_lookup_section(ctx, pkg, "reference_path"); + if (!ref_path_section) { + dmlog_error("reference_path section not found in %s", UCI_PACKAGE); + uci_free_context(ctx); + free(linker_string); + free(hash_path); + return -1; + } + + const char *uci_path = uci_lookup_option_string(ctx, ref_path_section, hash_path); + + int ret = -1; /* default: not resolved */ + + if (uci_path) { + /* Matching object path found */ + *object_path = strdup(uci_path); + if (*object_path) + ret = 0; + } else { + /* No match. Leave *object_path NULL and return error */ + ret = -1; + } + + uci_free_context(ctx); + free(linker_string); + free(hash_path); + + return ret; +} + diff --git a/dm-framework/dm-api/src/core/dm_linker.h b/dm-framework/dm-api/src/core/dm_linker.h index d3ad99027..f79963d89 100755 --- a/dm-framework/dm-api/src/core/dm_linker.h +++ b/dm-framework/dm-api/src/core/dm_linker.h @@ -74,6 +74,14 @@ int object_has_linker_param(dm_node_id_t object_id); */ int dm_resolve_linker(const char *object_path, char **value); +/* + * Resolve object path from a linker value. + * Returns 0 and sets *object_path on success. + * Returns -1 and leaves *object_path NULL if no matching object path exists + * or on error. + */ +int dm_resolve_linker_path(const char *base_path, const char *key_name, const char *key_value, char **object_path); + #ifdef __cplusplus } #endif diff --git a/dm-framework/dm-api/src/quickjs/qjs.c b/dm-framework/dm-api/src/quickjs/qjs.c index 6de2d901b..67724e956 100644 --- a/dm-framework/dm-api/src/quickjs/qjs.c +++ b/dm-framework/dm-api/src/quickjs/qjs.c @@ -25,6 +25,8 @@ #include "dm_uci.h" #include "inode_buf.h" #include "qjs_api.h" +// Forward declaration from qjs_uci_api.c +extern int qjs_uci_api_init(); static JSRuntime *runtime_g; static JSContext *ctx_g; @@ -184,12 +186,13 @@ static void init_qjs_c_api() qjs_log_api_init(); qjs_dm_api_init(); qjs_ubus_api_init(); + qjs_uci_api_init(); } static int import_js_handlers() { const char *str = - "import * as dm_handlers from '/usr/lib/quickjs/dm_handlers/exports.js';\n" + "import * as dm_handlers from '/usr/lib/dmf_handlers/exports.js';\n" "globalThis.dm_handlers = dm_handlers;\n"; JSValue val = qjs_eval_buf(str, strlen(str), "", JS_EVAL_TYPE_MODULE); if (JS_IsException(val)) { diff --git a/dm-framework/dm-api/src/quickjs/qjs_dm_api.c b/dm-framework/dm-api/src/quickjs/qjs_dm_api.c index 0a71de412..16b4703bf 100644 --- a/dm-framework/dm-api/src/quickjs/qjs_dm_api.c +++ b/dm-framework/dm-api/src/quickjs/qjs_dm_api.c @@ -534,6 +534,36 @@ static JSValue _dm_resolve(JSContext *ctx, JSValueConst this_val, return js_val; } +static JSValue _dm_resolve_path(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 3 || !JS_IsString(argv[0]) || !JS_IsString(argv[1]) || !JS_IsString(argv[2])) { + dmlog_info("_dm_resolve_path, invalid argument"); + return JS_UNDEFINED; + } + + const char *base_path = JS_ToCString(ctx, argv[0]); + const char *key_name = JS_ToCString(ctx, argv[1]); + const char *key_value = JS_ToCString(ctx, argv[2]); + + char *obj_path = NULL; + if (dm_resolve_linker_path(base_path, key_name, key_value, &obj_path) != 0 || obj_path == NULL) { + JS_FreeCString(ctx, base_path); + JS_FreeCString(ctx, key_name); + JS_FreeCString(ctx, key_value); + return JS_UNDEFINED; + } + + JSValue js_val = JS_NewString(ctx, obj_path); + + free(obj_path); + JS_FreeCString(ctx, base_path); + JS_FreeCString(ctx, key_name); + JS_FreeCString(ctx, key_value); + + return js_val; +} + int qjs_dm_api_init() { qjs_register_c_api("_dm_get", _dm_get, 1); @@ -545,5 +575,6 @@ int qjs_dm_api_init() qjs_register_c_api("_dm_update", _dm_update, 1); qjs_register_c_api("_dm_apply", _dm_apply, 1); qjs_register_c_api("_dm_linker_value", _dm_resolve, 1); + qjs_register_c_api("_dm_linker_path", _dm_resolve_path, 3); return 0; } diff --git a/dm-framework/dm-api/src/quickjs/qjs_uci_api.c b/dm-framework/dm-api/src/quickjs/qjs_uci_api.c new file mode 100755 index 000000000..895cb98a4 --- /dev/null +++ b/dm-framework/dm-api/src/quickjs/qjs_uci_api.c @@ -0,0 +1,430 @@ + +#include +#include +#include +#include +#include +#include + +#include "dm_log.h" +#include "qjs.h" + +/* + * Simple libuci based implementation to offer a subset of the ubus–uci RPC + * functions directly to QuickJS without going through ubus. The goal is to + * be feature-parity with what dm-files/uci.js currently needs: get, set, add, + * delete, commit, revert, changes. All logic is consolidated in the single + * entry point _uci_call(, ). The function returns an + * array [ret, res] where ret == 0 upon success and res carries any return + * payload – this mirrors the behaviour of _ubus_call so the existing JS glue + * can stay unchanged apart from calling _uci_call instead. + * + * NOTE: This is intentionally a *minimal* implementation – only the argument + * variations that are used in our JavaScript helper (uci2.js) are handled: + * – get : { config, section?, option? } + * – set : { config, section, values } + * – add : { config, type, name?, values? } + * – delete : { config, section, option? } + * – commit / revert / changes : { config } + * Unsupported combinations or failures result in ret == -1. + */ + +static const char *js_obj_get_str(JSContext *ctx, JSValue obj, const char *name) +{ + JSValue val = JS_GetPropertyStr(ctx, obj, name); + if (JS_IsUndefined(val)) { + return NULL; + } + const char *str = JS_ToCString(ctx, val); + JS_FreeValue(ctx, val); + return str; /* caller must free via JS_FreeCString */ +} + +static JSValue create_values_object(JSContext *ctx, struct uci_section *s) +{ + JSValue js_obj = JS_NewObject(ctx); + if (!s) + return js_obj; + + /* meta fields similar to ubus */ + JS_SetPropertyStr(ctx, js_obj, ".anonymous", JS_NewBool(ctx, s->anonymous)); + JS_SetPropertyStr(ctx, js_obj, ".type", JS_NewString(ctx, s->type)); + JS_SetPropertyStr(ctx, js_obj, ".name", JS_NewString(ctx, s->e.name)); + + struct uci_element *oe; + uci_foreach_element(&s->options, oe) { + struct uci_option *o = uci_to_option(oe); + if (!o || o->type != UCI_TYPE_STRING) + continue; /* ignore lists for now */ + JS_SetPropertyStr(ctx, js_obj, o->e.name, JS_NewString(ctx, o->v.string)); + } + return js_obj; +} + +static int js_obj_foreach(JSContext *ctx, JSValue obj, + int (*cb)(JSContext *, const char *, JSValue, void *), + void *opaque) +{ + JSPropertyEnum *props; + uint32_t len; + int res = 0; + if (JS_GetOwnPropertyNames(ctx, &props, &len, obj, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) + return -1; + for (uint32_t i = 0; i < len; i++) { + const char *key = JS_AtomToCString(ctx, props[i].atom); + JSValue val = JS_GetPropertyStr(ctx, obj, key); + if (cb(ctx, key, val, opaque) != 0) { + res = -1; + JS_FreeCString(ctx, key); + JS_FreeValue(ctx, val); + break; + } + JS_FreeCString(ctx, key); + JS_FreeValue(ctx, val); + } + js_free(ctx, props); + return res; +} + +/* callback context for set/add */ +struct set_ctx { + struct uci_context *uci; + const char *config; + const char *section; +}; + +static int set_option_cb(JSContext *ctx, const char *key, JSValue val, void *opaque) +{ + struct set_ctx *sctx = (struct set_ctx *)opaque; + const char *vstr = JS_ToCString(ctx, val); + if (!vstr) + return -1; + + char path[256]; + snprintf(path, sizeof(path), "%s.%s.%s", sctx->config, sctx->section, key); + + struct uci_ptr ptr = {}; + if (uci_lookup_ptr(sctx->uci, &ptr, path, true) != UCI_OK) { + JS_FreeCString(ctx, vstr); + return -1; + } + ptr.value = vstr; + if (uci_set(sctx->uci, &ptr) != UCI_OK) { + JS_FreeCString(ctx, vstr); + return -1; + } + JS_FreeCString(ctx, vstr); + return 0; +} + +static int section_matches(JSContext *ctx, JSValue match_obj, struct uci_section *s) +{ + /* No match criteria -> everything matches */ + if (JS_IsUndefined(match_obj) || JS_IsNull(match_obj) || !JS_IsObject(match_obj)) + return 1; + + JSPropertyEnum *props; + uint32_t len; + if (JS_GetOwnPropertyNames(ctx, &props, &len, match_obj, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) + return 0; + + /* Iterate over every field in the match object and ensure the option exists + * and contains exactly the requested value. Supports both string and list + * option types. + */ + for (uint32_t i = 0; i < len; i++) { + const char *key = JS_AtomToCString(ctx, props[i].atom); + JSValue val = JS_GetPropertyStr(ctx, match_obj, key); + const char *val_str = JS_ToCString(ctx, val); + + /* Retrieve the option */ + struct uci_option *opt = uci_lookup_option(s->package->ctx, s, key); + if (!opt) { + /* option missing -> no match */ + JS_FreeCString(ctx, key); + JS_FreeCString(ctx, val_str); + JS_FreeValue(ctx, val); + js_free(ctx, props); + return 0; + } + + bool this_ok = false; + if (opt->type == UCI_TYPE_STRING) { + this_ok = (strcmp(opt->v.string, val_str) == 0); + } else if (opt->type == UCI_TYPE_LIST) { + /* check if value appears in the list */ + struct uci_element *le; + uci_foreach_element(&opt->v.list, le) { + if (strcmp(le->name, val_str) == 0) { + this_ok = true; + break; + } + } + } + + JS_FreeCString(ctx, key); + JS_FreeCString(ctx, val_str); + JS_FreeValue(ctx, val); + + /* Early exit on first mismatch */ + if (!this_ok) { + js_free(ctx, props); + return 0; + } + } + + js_free(ctx, props); + return 1; +} + +static JSValue _uci_call(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + int ret = -1; + JSValue res_val = JS_UNDEFINED; + + if (argc < 1 || !JS_IsString(argv[0])) { + dmlog_error("_uci_call: invalid arguments"); + goto end; + } + + const char *method = JS_ToCString(ctx, argv[0]); + JSValue args_obj = (argc > 1 && JS_IsObject(argv[1])) ? argv[1] : JS_UNDEFINED; + struct uci_context *uci = uci_alloc_context(); + if (!uci) { + dmlog_error("uci_alloc_context failed"); + goto end_method; + } + + /* Optional custom configuration directory */ + const char *confdir = NULL; + if (args_obj != JS_UNDEFINED) { + confdir = js_obj_get_str(ctx, args_obj, "confdir"); /* explicit */ + } + + /* Support absolute/relative file path in 'config' by splitting dir/name */ + const char *config = (args_obj != JS_UNDEFINED) ? js_obj_get_str(ctx, args_obj, "config") : NULL; + + char cfg_name_buf[128]; + if (config && strchr(config, '/')) { + /* treat as path */ + const char *last_slash = strrchr(config, '/'); + size_t dir_len = last_slash - config; + if (!confdir) { + confdir = strndup(config, dir_len); + } + snprintf(cfg_name_buf, sizeof(cfg_name_buf), "%s", last_slash + 1); + /* drop possible file extension; uci expects plain filename */ + config = cfg_name_buf; + } + + if (confdir) { + uci_set_confdir(uci, confdir); + } + + if (!config && strcmp(method, "configs") != 0) { + dmlog_error("_uci_call: 'config' is missing for method %s", method); + goto cleanup; + } + + struct uci_package *pkg = NULL; + if (config && uci_load(uci, config, &pkg) != UCI_OK) { + dmlog_error("uci_load failed for %s", config); + goto cleanup; + } + + if (strcmp(method, "get") == 0) { + const char *section = js_obj_get_str(ctx, args_obj, "section"); + const char *option = js_obj_get_str(ctx, args_obj, "option"); + + if (option && !section) { + /* option without section is invalid */ + dmlog_error("_uci_call get: option requires section"); + goto cleanup; + } + + if (option) { + char path[256]; + snprintf(path, sizeof(path), "%s.%s.%s", config, section, option); + struct uci_ptr ptr = {}; + if (uci_lookup_ptr(uci, &ptr, path, true) != UCI_OK || !ptr.o) { + dmlog_error("uci_lookup_ptr failed for %s", path); + goto cleanup; + } + res_val = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, res_val, "value", JS_NewString(ctx, ptr.o->v.string)); + ret = 0; + } else if (section) { + /* return all options of the section */ + struct uci_section *s = uci_lookup_section(uci, pkg, section); + if (!s) { + dmlog_error("section %s not found", section); + goto cleanup; + } + JSValue vals = create_values_object(ctx, s); + res_val = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, res_val, "values", vals); + ret = 0; + } else { + /* package wide query, possibly filtered by type/match */ + const char *type_filter = js_obj_get_str(ctx, args_obj, "type"); + JSValue match_obj = JS_GetPropertyStr(ctx, args_obj, "match"); + + JSValue vals = JS_NewObject(ctx); + struct uci_element *e; + uci_foreach_element(&pkg->sections, e) { + struct uci_section *s = uci_to_section(e); + if (type_filter && strcmp(s->type, type_filter) != 0) + continue; + if (!section_matches(ctx, match_obj, s)) + continue; + JS_SetPropertyStr(ctx, vals, s->e.name, create_values_object(ctx, s)); + } + + res_val = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, res_val, "values", vals); + ret = 0; + + if (type_filter) JS_FreeCString(ctx, type_filter); + JS_FreeValue(ctx, match_obj); + } + } else if (strcmp(method, "set") == 0) { + const char *section = js_obj_get_str(ctx, args_obj, "section"); + if (!section) { + dmlog_error("_uci_call set: section missing"); + goto cleanup; + } + JSValue values_obj = JS_GetPropertyStr(ctx, args_obj, "values"); + if (!JS_IsObject(values_obj)) { + dmlog_error("_uci_call set: values not object"); + JS_FreeValue(ctx, values_obj); + goto cleanup; + } + struct set_ctx sctx = { .uci = uci, .config = config, .section = section }; + if (js_obj_foreach(ctx, values_obj, set_option_cb, &sctx) != 0) { + JS_FreeValue(ctx, values_obj); + goto cleanup; + } + JS_FreeValue(ctx, values_obj); + if (uci_save(uci, pkg) != UCI_OK) { + dmlog_error("uci_save failed for %s", config); + goto cleanup; + } + ret = 0; /* no res */ + } else if (strcmp(method, "add") == 0) { + const char *type = js_obj_get_str(ctx, args_obj, "type"); + if (!type) { + dmlog_error("_uci_call add: type missing"); + goto cleanup; + } + const char *name = js_obj_get_str(ctx, args_obj, "name"); + + struct uci_section *s; + if (uci_add_section(uci, pkg, type, &s) != UCI_OK) { + dmlog_error("uci_add_section failed"); + goto cleanup; + } + if (name) { + /* rename newly created section to requested name */ + char tmp[256]; + snprintf(tmp, sizeof(tmp), "%s.%s", config, s->e.name); + struct uci_ptr rptr = {}; + if (uci_lookup_ptr(uci, &rptr, tmp, true) != UCI_OK) { + dmlog_error("rename lookup failed"); + goto cleanup; + } + rptr.value = name; + if (uci_rename(uci, &rptr) != UCI_OK) { + dmlog_error("uci_rename failed"); + goto cleanup; + } + /* section name changed - use new name for response */ + snprintf(tmp, sizeof(tmp), "%s", name); + res_val = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, res_val, "section", JS_NewString(ctx, tmp)); + } else { + res_val = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, res_val, "section", JS_NewString(ctx, s->e.name)); + } + /* optional values */ + JSValue values_obj = JS_GetPropertyStr(ctx, args_obj, "values"); + if (JS_IsObject(values_obj)) { + struct set_ctx sctx = { .uci = uci, .config = config, .section = s->e.name }; + js_obj_foreach(ctx, values_obj, set_option_cb, &sctx); + } + JS_FreeValue(ctx, values_obj); + uci_save(uci, pkg); + ret = 0; + } else if (strcmp(method, "delete") == 0) { + const char *section = js_obj_get_str(ctx, args_obj, "section"); + const char *option = js_obj_get_str(ctx, args_obj, "option"); + if (!section) { + dmlog_error("_uci_call delete: section missing"); + goto cleanup; + } + char path[256]; + if (option) + snprintf(path, sizeof(path), "%s.%s.%s", config, section, option); + else + snprintf(path, sizeof(path), "%s.%s", config, section); + struct uci_ptr ptr = {}; + if (uci_lookup_ptr(uci, &ptr, path, true) != UCI_OK) { + dmlog_error("uci_lookup_ptr failed for delete %s", path); + goto cleanup; + } + if (uci_delete(uci, &ptr) != UCI_OK) { + dmlog_error("uci_delete failed"); + goto cleanup; + } + uci_save(uci, pkg); + ret = 0; + } else if (strcmp(method, "commit") == 0) { + if (uci_commit(uci, &pkg, false) != UCI_OK) { + dmlog_error("uci_commit failed"); + goto cleanup; + } + ret = 0; + } else if (strcmp(method, "revert") == 0) { + struct uci_ptr rptr = {}; + char path[128]; + snprintf(path, sizeof(path), "%s", config); + if (uci_lookup_ptr(uci, &rptr, path, true) != UCI_OK) { + dmlog_error("revert lookup failed"); + goto cleanup; + } + if (uci_revert(uci, &rptr) != UCI_OK) { + dmlog_error("uci_revert failed"); + goto cleanup; + } + ret = 0; + } else if (strcmp(method, "changes") == 0) { + /* Provide minimal stub: always empty */ + res_val = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, res_val, "changes", JS_NewArray(ctx)); + ret = 0; + } else { + dmlog_error("_uci_call: unsupported method %s", method); + } + +cleanup: + if (config && config != cfg_name_buf) + JS_FreeCString(ctx, config); + if (confdir) + JS_FreeCString(ctx, confdir); + if (uci) + uci_free_context(uci); + +end_method: + JS_FreeCString(ctx, method); +end: + JSValue arr = JS_NewArray(ctx); + JS_SetPropertyUint32(ctx, arr, 0, JS_NewInt32(ctx, ret)); + JS_SetPropertyUint32(ctx, arr, 1, res_val); + return arr; +} + +int qjs_uci_api_init() +{ + qjs_register_c_api("_uci_call", _uci_call, 2); + return 0; +} \ No newline at end of file