refactor and fix

This commit is contained in:
Meng 2025-07-15 13:27:42 +02:00
parent 53e743d0b2
commit 24646d3367
23 changed files with 844 additions and 144 deletions

View file

@ -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)))

Binary file not shown.

View file

@ -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',

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

Binary file not shown.

View file

@ -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) {

View file

@ -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();
}
})();

View file

@ -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

View file

@ -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
}

View file

@ -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)

View file

@ -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) {

View file

@ -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
* "<base_path>[<key_name>==<key_value>]."
* 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;
}

View file

@ -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

View file

@ -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), "<get_js_init>", JS_EVAL_TYPE_MODULE);
if (JS_IsException(val)) {

View file

@ -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;
}

View file

@ -0,0 +1,430 @@
#include <quickjs/quickjs-libc.h>
#include <quickjs/quickjs.h>
#include <uci.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include "dm_log.h"
#include "qjs.h"
/*
* Simple libuci based implementation to offer a subset of the ubusuci 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(<method>, <argsObject>). 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;
}