mirror of https://github.com/nirenjan/libx52.git
feat: Migrate daemon to use localipc library
parent
b8f059a881
commit
4ab8da6680
|
|
@ -988,8 +988,9 @@ FILE_PATTERNS = libx52.h \
|
||||||
libx52util.h \
|
libx52util.h \
|
||||||
vkm.h \
|
vkm.h \
|
||||||
x52_cli.c \
|
x52_cli.c \
|
||||||
daemon_control.c \
|
daemon/x52ctl.dox \
|
||||||
x52dcomm.h \
|
x52dcomm.h \
|
||||||
|
x52d_ipc.h \
|
||||||
*.dox
|
*.dox
|
||||||
|
|
||||||
# The RECURSIVE tag can be used to specify whether or not subdirectories should
|
# The RECURSIVE tag can be used to specify whether or not subdirectories should
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@
|
||||||
#include <sys/un.h>
|
#include <sys/un.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <localipc/lipc.h>
|
||||||
#include <libx52/x52dcomm.h>
|
#include <libx52/x52dcomm.h>
|
||||||
|
#include <libx52/x52d_ipc.h>
|
||||||
#include <daemon/x52dcomm-internal.h>
|
#include <daemon/x52dcomm-internal.h>
|
||||||
|
|
||||||
static int _setup_socket(struct sockaddr_un *remote, int len)
|
static int _setup_socket(struct sockaddr_un *remote, int len)
|
||||||
|
|
@ -71,6 +73,71 @@ int x52d_dial_notify(const char *sock_path)
|
||||||
return _setup_socket(&remote, len);
|
return _setup_socket(&remote, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char *x52d_ipc_socket_path(const char *sock_path)
|
||||||
|
{
|
||||||
|
return x52d_ipc_sock_path(sock_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
int x52d_dial_ipc(const char *sock_path)
|
||||||
|
{
|
||||||
|
const char *path = x52d_ipc_sock_path(sock_path);
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
fd = lipc_socket_connect(path, LIPC_SOCKET_CLOEXEC);
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x52d_ipc_device_state_decode(const lipc_header *hdr, const void *payload, size_t payload_len,
|
||||||
|
int *connected, uint16_t *vid, uint16_t *pid, const char **name_utf8, size_t *name_len)
|
||||||
|
{
|
||||||
|
if (!hdr) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (payload_len > 0 && !payload) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (hdr->tid != 0 || hdr->request != X52D_IPC_PUSH_DEVICE_STATE) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (hdr->index > 1u) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (hdr->length != (uint32_t)payload_len) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (connected) {
|
||||||
|
*connected = (hdr->index == 1u) ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (vid || pid) {
|
||||||
|
x52d_ipc_device_state_unpack_usb(hdr->value, vid, pid);
|
||||||
|
}
|
||||||
|
if (name_utf8) {
|
||||||
|
*name_utf8 = (payload_len > 0) ? (const char *)payload : NULL;
|
||||||
|
}
|
||||||
|
if (name_len) {
|
||||||
|
*name_len = payload_len;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lipc_status x52d_ipc_call(int fd, uint16_t request_id, uint16_t index, uint64_t value,
|
||||||
|
const void *payload, size_t payload_len,
|
||||||
|
lipc_header *reply_hdr, void *reply_payload, size_t reply_payload_cap, size_t *reply_len)
|
||||||
|
{
|
||||||
|
lipc_client *client;
|
||||||
|
lipc_status st;
|
||||||
|
|
||||||
|
client = lipc_client_create(0, NULL, NULL);
|
||||||
|
if (!client) {
|
||||||
|
return LIPC_INTERNAL_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
st = lipc_client_call(client, fd, request_id, index, value, payload, payload_len,
|
||||||
|
reply_hdr, reply_payload, reply_payload_cap, reply_len);
|
||||||
|
lipc_client_destroy(client);
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen)
|
int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen)
|
||||||
{
|
{
|
||||||
int msglen;
|
int msglen;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,15 @@ const char * x52d_notify_sock_path(const char *sock_path)
|
||||||
return sock_path;
|
return sock_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char *x52d_ipc_sock_path(const char *sock_path)
|
||||||
|
{
|
||||||
|
if (sock_path == NULL) {
|
||||||
|
sock_path = X52D_SOCK_IPC;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sock_path;
|
||||||
|
}
|
||||||
|
|
||||||
static int _setup_sockaddr(struct sockaddr_un *remote, const char *sock_path)
|
static int _setup_sockaddr(struct sockaddr_un *remote, const char *sock_path)
|
||||||
{
|
{
|
||||||
int len;
|
int len;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
"""LIPC wire identifiers and helpers for x52ctl.
|
||||||
|
|
||||||
|
Keep numeric values in sync with include/libx52/x52d_ipc.h and the daemon
|
||||||
|
registry (daemon/config_registry.json / generated config-defs).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class IpcRequest(IntEnum):
|
||||||
|
"""Framed IPC request opcodes (lipc_header.request)."""
|
||||||
|
|
||||||
|
CONFIG_LOAD = 0x01
|
||||||
|
CONFIG_RELOAD = 0x02
|
||||||
|
CONFIG_RESET = 0x03
|
||||||
|
CONFIG_CLEAR = 0x04
|
||||||
|
CONFIG_SAVE = 0x05
|
||||||
|
CONFIG_DUMP = 0x06
|
||||||
|
CONFIG_SET = 0x07
|
||||||
|
CONFIG_GET = 0x08
|
||||||
|
LOGGING_SHOW = 0x11
|
||||||
|
LOGGING_SET = 0x12
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigClearTarget(IntEnum):
|
||||||
|
"""lipc_header.index for CONFIG_CLEAR."""
|
||||||
|
|
||||||
|
STATE = 1
|
||||||
|
SYSCONF = 2
|
||||||
|
|
||||||
|
|
||||||
|
class IpcPush(IntEnum):
|
||||||
|
"""Server push request ids (tid == 0)."""
|
||||||
|
|
||||||
|
DEVICE_STATE = 0x8001
|
||||||
|
|
||||||
|
|
||||||
|
# Same sentinel as module-map.h / x52d_ipc logging selectors.
|
||||||
|
X52D_MOD_GLOBAL = 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
def device_state_pack_usb(vendor_id: int, product_id: int) -> int:
|
||||||
|
"""Pack 16-bit USB ids into lipc_header.value lower 32 bits (host order)."""
|
||||||
|
return int(((vendor_id & 0xFFFF) << 16) | (product_id & 0xFFFF))
|
||||||
249
daemon/config.c
249
daemon/config.c
|
|
@ -8,6 +8,13 @@
|
||||||
|
|
||||||
#include "build-config.h"
|
#include "build-config.h"
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#define PINELOG_MODULE X52D_MOD_CONFIG
|
#define PINELOG_MODULE X52D_MOD_CONFIG
|
||||||
#include "pinelog.h"
|
#include "pinelog.h"
|
||||||
|
|
@ -15,6 +22,28 @@
|
||||||
#include <daemon/constants.h>
|
#include <daemon/constants.h>
|
||||||
|
|
||||||
static struct x52d_config x52d_config;
|
static struct x52d_config x52d_config;
|
||||||
|
static const char *ipc_save_path;
|
||||||
|
|
||||||
|
void x52d_config_set_ipc_save_path(const char *path)
|
||||||
|
{
|
||||||
|
ipc_save_path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x52d_config_save_session(void)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
if (ipc_save_path != NULL) {
|
||||||
|
rc = x52d_config_save_file(&x52d_config, ipc_save_path);
|
||||||
|
if (rc != 0) {
|
||||||
|
PINELOG_ERROR(_("Error %d saving configuration file: %s"),
|
||||||
|
rc, strerror(rc));
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return x52d_config_save_state_atomic();
|
||||||
|
}
|
||||||
|
|
||||||
void x52d_config_load(const char *cfg_file)
|
void x52d_config_load(const char *cfg_file)
|
||||||
{
|
{
|
||||||
|
|
@ -110,3 +139,223 @@ void x52d_config_apply(void)
|
||||||
x52d_cfg_set_ ## section ## _ ## key(x52d_config . name);
|
x52d_cfg_set_ ## section ## _ ## key(x52d_config . name);
|
||||||
#include <daemon/config.def>
|
#include <daemon/config.def>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int mkdir_p(char *path)
|
||||||
|
{
|
||||||
|
char *p;
|
||||||
|
int saved_errno;
|
||||||
|
|
||||||
|
for (p = path + 1; *p != '\0'; p++) {
|
||||||
|
if (*p != '/') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
*p = '\0';
|
||||||
|
if (mkdir(path, 0755) != 0 && errno != EEXIST) {
|
||||||
|
saved_errno = errno;
|
||||||
|
*p = '/';
|
||||||
|
return saved_errno != 0 ? saved_errno : EIO;
|
||||||
|
}
|
||||||
|
*p = '/';
|
||||||
|
}
|
||||||
|
if (mkdir(path, 0755) != 0 && errno != EEXIST) {
|
||||||
|
return errno != 0 ? errno : EIO;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ensure_dir_for_file(const char *filepath)
|
||||||
|
{
|
||||||
|
char buf[PATH_MAX];
|
||||||
|
char *slash;
|
||||||
|
|
||||||
|
if (filepath == NULL || filepath[0] == '\0') {
|
||||||
|
return EINVAL;
|
||||||
|
}
|
||||||
|
if (strlen(filepath) >= sizeof(buf)) {
|
||||||
|
return ENAMETOOLONG;
|
||||||
|
}
|
||||||
|
memcpy(buf, filepath, strlen(filepath) + 1);
|
||||||
|
slash = strrchr(buf, '/');
|
||||||
|
if (slash == NULL || slash == buf) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
*slash = '\0';
|
||||||
|
return mkdir_p(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
int x52d_config_reload_canonical(void)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
const char *chosen = NULL;
|
||||||
|
|
||||||
|
rc = x52d_config_set_defaults(&x52d_config);
|
||||||
|
if (rc != 0) {
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access(X52D_STATE_CFG_FILE, R_OK) == 0) {
|
||||||
|
chosen = X52D_STATE_CFG_FILE;
|
||||||
|
} else if (access(X52D_SYS_CFG_FILE, R_OK) == 0) {
|
||||||
|
chosen = X52D_SYS_CFG_FILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosen != NULL) {
|
||||||
|
rc = x52d_config_load_file(&x52d_config, chosen);
|
||||||
|
if (rc != 0) {
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = x52d_config_apply_overrides(&x52d_config);
|
||||||
|
x52d_config_clear_overrides();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x52d_config_load_from_path(const char *path)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
if (path == NULL || path[0] == '\0') {
|
||||||
|
return EINVAL;
|
||||||
|
}
|
||||||
|
if (access(path, R_OK) != 0) {
|
||||||
|
return errno != 0 ? errno : EACCES;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = x52d_config_set_defaults(&x52d_config);
|
||||||
|
if (rc != 0) {
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = x52d_config_load_file(&x52d_config, path);
|
||||||
|
if (rc != 0) {
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = x52d_config_apply_overrides(&x52d_config);
|
||||||
|
x52d_config_clear_overrides();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x52d_config_reset_to_defaults(void)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
rc = x52d_config_set_defaults(&x52d_config);
|
||||||
|
if (rc != 0) {
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
rc = x52d_config_apply_overrides(&x52d_config);
|
||||||
|
x52d_config_clear_overrides();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x52d_config_dump_to_alloc(char **out, size_t *out_len)
|
||||||
|
{
|
||||||
|
FILE *fp;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
if (out == NULL || out_len == NULL) {
|
||||||
|
return EINVAL;
|
||||||
|
}
|
||||||
|
*out = NULL;
|
||||||
|
*out_len = 0;
|
||||||
|
|
||||||
|
fp = open_memstream(out, out_len);
|
||||||
|
if (fp == NULL) {
|
||||||
|
return errno != 0 ? errno : ENOMEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = x52d_config_write_ini(&x52d_config, fp, "(memory)");
|
||||||
|
if (fclose(fp) != 0) {
|
||||||
|
if (rc == 0) {
|
||||||
|
rc = errno != 0 ? errno : EIO;
|
||||||
|
}
|
||||||
|
free(*out);
|
||||||
|
*out = NULL;
|
||||||
|
*out_len = 0;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x52d_config_save_state_atomic(void)
|
||||||
|
{
|
||||||
|
char template[PATH_MAX];
|
||||||
|
int fd;
|
||||||
|
FILE *fp;
|
||||||
|
int rc;
|
||||||
|
int errsv;
|
||||||
|
|
||||||
|
rc = ensure_dir_for_file(X52D_STATE_CFG_FILE);
|
||||||
|
if (rc != 0) {
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snprintf(template, sizeof template, "%s.XXXXXX", X52D_STATE_CFG_FILE) >= (int)sizeof(template)) {
|
||||||
|
return ENAMETOOLONG;
|
||||||
|
}
|
||||||
|
|
||||||
|
fd = mkstemp(template);
|
||||||
|
if (fd < 0) {
|
||||||
|
return errno != 0 ? errno : EIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
fp = fdopen(fd, "w");
|
||||||
|
if (fp == NULL) {
|
||||||
|
errsv = errno;
|
||||||
|
close(fd);
|
||||||
|
unlink(template);
|
||||||
|
return errsv != 0 ? errsv : EIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = x52d_config_write_ini(&x52d_config, fp, template);
|
||||||
|
if (fflush(fp) != 0 && rc == 0) {
|
||||||
|
rc = errno != 0 ? errno : EIO;
|
||||||
|
}
|
||||||
|
if (fsync(fileno(fp)) != 0 && rc == 0) {
|
||||||
|
rc = errno != 0 ? errno : EIO;
|
||||||
|
}
|
||||||
|
if (fclose(fp) != 0 && rc == 0) {
|
||||||
|
rc = errno != 0 ? errno : EIO;
|
||||||
|
}
|
||||||
|
if (rc != 0) {
|
||||||
|
unlink(template);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rename(template, X52D_STATE_CFG_FILE) != 0) {
|
||||||
|
errsv = errno;
|
||||||
|
unlink(template);
|
||||||
|
return errsv != 0 ? errsv : EIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x52d_config_clear_disk_then_reload(uint16_t target, int *out_unlink_errno)
|
||||||
|
{
|
||||||
|
const char *path = NULL;
|
||||||
|
int reload_rc;
|
||||||
|
|
||||||
|
if (out_unlink_errno != NULL) {
|
||||||
|
*out_unlink_errno = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == X52D_CONFIG_CLEAR_TARGET_STATE) {
|
||||||
|
path = X52D_STATE_CFG_FILE;
|
||||||
|
} else if (target == X52D_CONFIG_CLEAR_TARGET_SYSCONF) {
|
||||||
|
path = X52D_SYS_CFG_FILE;
|
||||||
|
} else {
|
||||||
|
return EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unlink(path) != 0 && errno != ENOENT) {
|
||||||
|
if (out_unlink_errno != NULL) {
|
||||||
|
*out_unlink_errno = errno != 0 ? errno : EIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_rc = x52d_config_reload_canonical();
|
||||||
|
return reload_rc;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#ifndef X52D_CONFIG_H
|
#ifndef X52D_CONFIG_H
|
||||||
#define X52D_CONFIG_H
|
#define X52D_CONFIG_H
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
|
@ -106,9 +107,78 @@ void x52d_config_apply_immediate(const char *section, const char *key);
|
||||||
void x52d_config_apply(void);
|
void x52d_config_apply(void);
|
||||||
|
|
||||||
int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file);
|
int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file);
|
||||||
|
|
||||||
|
/** Write the full active configuration as INI to @p cfg_fp (for save or in-memory dump). */
|
||||||
|
int x52d_config_write_ini(struct x52d_config *cfg, FILE *cfg_fp, const char *path_label);
|
||||||
|
|
||||||
void x52d_config_save(const char *cfg_file);
|
void x52d_config_save(const char *cfg_file);
|
||||||
|
|
||||||
int x52d_config_set(const char *section, const char *key, const char *value);
|
int x52d_config_set(const char *section, const char *key, const char *value);
|
||||||
const char *x52d_config_get(const char *section, const char *key);
|
const char *x52d_config_get(const char *section, const char *key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload configuration using the canonical order: state file if present and readable,
|
||||||
|
* else system config if present, else in-memory defaults (plus CLI overrides once).
|
||||||
|
*
|
||||||
|
* @return 0 on success, or a positive errno-style code on failure.
|
||||||
|
*/
|
||||||
|
int x52d_config_reload_canonical(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load defaults, then load @p path (must be readable). Applies CLI overrides.
|
||||||
|
*
|
||||||
|
* @return 0 on success, or a positive errno-style code on failure.
|
||||||
|
*/
|
||||||
|
int x52d_config_load_from_path(const char *path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset active configuration to defaults and re-apply CLI overrides.
|
||||||
|
*
|
||||||
|
* @return 0 on success, or a positive errno-style code on failure.
|
||||||
|
*/
|
||||||
|
int x52d_config_reset_to_defaults(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the active configuration as INI text into a heap buffer.
|
||||||
|
* On success, @p *out is NUL-terminated (length includes the final NUL in @p *out_len).
|
||||||
|
*
|
||||||
|
* @return 0 on success, or a positive errno-style code on failure (@p *out undefined).
|
||||||
|
*/
|
||||||
|
int x52d_config_dump_to_alloc(char **out, size_t *out_len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically write the active configuration to @ref X52D_STATE_CFG_FILE
|
||||||
|
* (temp file in the same directory + rename).
|
||||||
|
*
|
||||||
|
* @return 0 on success, or a positive errno-style code on failure.
|
||||||
|
*/
|
||||||
|
int x52d_config_save_state_atomic(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When non-NULL, @ref x52d_config_save_session writes to this path (same file as @c x52d -c).
|
||||||
|
* When NULL, @ref x52d_config_save_state_atomic is used.
|
||||||
|
*/
|
||||||
|
void x52d_config_set_ipc_save_path(const char *path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save active configuration: session path if set, otherwise @ref x52d_config_save_state_atomic.
|
||||||
|
*
|
||||||
|
* @return 0 on success, or a positive errno-style code on failure.
|
||||||
|
*/
|
||||||
|
int x52d_config_save_session(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete on-disk configuration selected by LIPC @c CONFIG_CLEAR index, then run
|
||||||
|
* @ref x52d_config_reload_canonical. @p target is @ref X52D_CONFIG_CLEAR_TARGET_STATE or
|
||||||
|
* @ref X52D_CONFIG_CLEAR_TARGET_SYSCONF.
|
||||||
|
*
|
||||||
|
* Unlink uses @c ENOENT as success (nothing to remove). If removal fails (e.g. read-only
|
||||||
|
* sysconf), the errno is stored in @p out_unlink_errno when non-@c NULL and reload still runs.
|
||||||
|
*
|
||||||
|
* @return @c 0 if reload succeeded, else a positive errno-style code from reload.
|
||||||
|
* @param out_unlink_errno optional; set to @c 0 if unlink succeeded or file was absent;
|
||||||
|
* otherwise set to errno from @c unlink (2); reload is still attempted.
|
||||||
|
*/
|
||||||
|
int x52d_config_clear_disk_then_reload(uint16_t target, int *out_unlink_errno);
|
||||||
|
|
||||||
#endif // !defined X52D_CONFIG_H
|
#endif // !defined X52D_CONFIG_H
|
||||||
|
|
|
||||||
|
|
@ -93,11 +93,52 @@ static const char * date_format_dumper(const char *section, const char *key, str
|
||||||
#undef CHECK_PARAMS
|
#undef CHECK_PARAMS
|
||||||
#undef CONFIG_PTR
|
#undef CONFIG_PTR
|
||||||
|
|
||||||
|
int x52d_config_write_ini(struct x52d_config *cfg, FILE *cfg_fp, const char *path_label)
|
||||||
|
{
|
||||||
|
char *current_section = NULL;
|
||||||
|
const char *value = "";
|
||||||
|
int out_rc = 0;
|
||||||
|
|
||||||
|
if (cfg == NULL || cfg_fp == NULL || path_label == NULL) {
|
||||||
|
return EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
PINELOG_TRACE("Writing configuration INI to %s", path_label);
|
||||||
|
#define CFG(section, key, name, type, def) do { \
|
||||||
|
if (current_section == NULL || strcasecmp(current_section, #section)) { \
|
||||||
|
if (current_section != NULL) { \
|
||||||
|
free(current_section); \
|
||||||
|
} \
|
||||||
|
current_section = strdup(#section); \
|
||||||
|
if (current_section == NULL) { \
|
||||||
|
value = NULL; \
|
||||||
|
out_rc = ENOMEM; \
|
||||||
|
goto exit_write_ini; \
|
||||||
|
} \
|
||||||
|
PINELOG_TRACE("Printing section header %s", #section); \
|
||||||
|
fprintf(cfg_fp, "[%s]\n", #section); \
|
||||||
|
} \
|
||||||
|
PINELOG_TRACE("Dumping " #section "." #key " to %s", path_label); \
|
||||||
|
value = type ## _dumper(#section, #key, cfg, offsetof(struct x52d_config, name)); \
|
||||||
|
if (value == NULL) { \
|
||||||
|
PINELOG_ERROR(_("Failed to dump %s.%s to config stream %s"), \
|
||||||
|
#section, #key, path_label); \
|
||||||
|
out_rc = EIO; \
|
||||||
|
goto exit_write_ini; \
|
||||||
|
} \
|
||||||
|
fprintf(cfg_fp, "%s = %s\n", #key, value); \
|
||||||
|
} while (0);
|
||||||
|
#include <daemon/config.def>
|
||||||
|
|
||||||
|
exit_write_ini:
|
||||||
|
free(current_section);
|
||||||
|
return out_rc;
|
||||||
|
}
|
||||||
|
|
||||||
int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file)
|
int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file)
|
||||||
{
|
{
|
||||||
FILE *cfg_fp;
|
FILE *cfg_fp;
|
||||||
char *current_section = NULL;
|
int rc;
|
||||||
const char *value;
|
|
||||||
|
|
||||||
if (cfg == NULL || cfg_file == NULL) {
|
if (cfg == NULL || cfg_file == NULL) {
|
||||||
return EINVAL;
|
return EINVAL;
|
||||||
|
|
@ -107,35 +148,14 @@ int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file)
|
||||||
if (cfg_fp == NULL) {
|
if (cfg_fp == NULL) {
|
||||||
PINELOG_ERROR(_("Unable to save config file %s - code %d: %s"),
|
PINELOG_ERROR(_("Unable to save config file %s - code %d: %s"),
|
||||||
cfg_file, errno, strerror(errno));
|
cfg_file, errno, strerror(errno));
|
||||||
return 1;
|
return errno != 0 ? errno : EIO;
|
||||||
}
|
}
|
||||||
|
|
||||||
PINELOG_TRACE("Saving configuration to file %s", cfg_file);
|
rc = x52d_config_write_ini(cfg, cfg_fp, cfg_file);
|
||||||
#define CFG(section, key, name, type, def) do { \
|
if (fclose(cfg_fp) != 0 && rc == 0) {
|
||||||
if (current_section == NULL || strcasecmp(current_section, #section)) { \
|
rc = errno != 0 ? errno : EIO;
|
||||||
if (current_section != NULL) { \
|
}
|
||||||
free(current_section); \
|
return rc;
|
||||||
} \
|
|
||||||
current_section = strdup(#section); \
|
|
||||||
PINELOG_TRACE("Printing section header %s", #section); \
|
|
||||||
fprintf(cfg_fp, "[%s]\n", #section); \
|
|
||||||
} \
|
|
||||||
PINELOG_TRACE("Dumping " #section "." #key " to file %s", cfg_file); \
|
|
||||||
value = type ## _dumper(#section, #key, cfg, offsetof(struct x52d_config, name)); \
|
|
||||||
if (value == NULL) { \
|
|
||||||
PINELOG_ERROR(_("Failed to dump %s.%s to config file %s"), \
|
|
||||||
#section, #key, cfg_file); \
|
|
||||||
goto exit_dump; \
|
|
||||||
} else { \
|
|
||||||
fprintf(cfg_fp, "%s = %s\n", #key, value); \
|
|
||||||
} \
|
|
||||||
} while (0);
|
|
||||||
#include <daemon/config.def>
|
|
||||||
|
|
||||||
exit_dump:
|
|
||||||
free(current_section);
|
|
||||||
fclose(cfg_fp);
|
|
||||||
return (value == NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *x52d_config_get_param(struct x52d_config *cfg, const char *section, const char *key)
|
const char *x52d_config_get_param(struct x52d_config *cfg, const char *section, const char *key)
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,30 @@
|
||||||
#ifndef X52D_CONST_H
|
#ifndef X52D_CONST_H
|
||||||
#define X52D_CONST_H
|
#define X52D_CONST_H
|
||||||
|
|
||||||
|
#include <libx52/x52d_ipc.h>
|
||||||
|
|
||||||
#define X52D_APP_NAME "x52d"
|
#define X52D_APP_NAME "x52d"
|
||||||
|
|
||||||
#define X52D_LOG_FILE LOGDIR "/" X52D_APP_NAME ".log"
|
#define X52D_LOG_FILE LOGDIR "/" X52D_APP_NAME ".log"
|
||||||
|
|
||||||
#define X52D_SYS_CFG_FILE SYSCONFDIR "/" X52D_APP_NAME "/" X52D_APP_NAME ".conf"
|
#define X52D_SYS_CFG_FILE SYSCONFDIR "/" X52D_APP_NAME "/" X52D_APP_NAME ".conf"
|
||||||
|
|
||||||
|
/** Persistent runtime configuration (not under ephemeral @c RUNDIR). */
|
||||||
|
#define X52D_STATE_CFG_FILE LOCALSTATEDIR "/lib/" X52D_APP_NAME "/" X52D_APP_NAME ".conf"
|
||||||
|
|
||||||
|
/** @deprecated Use @ref X52D_IPC_CONFIG_CLEAR_TARGET_STATE in @c libx52/x52d_ipc.h. */
|
||||||
|
#define X52D_CONFIG_CLEAR_TARGET_STATE X52D_IPC_CONFIG_CLEAR_TARGET_STATE
|
||||||
|
/** @deprecated Use @ref X52D_IPC_CONFIG_CLEAR_TARGET_SYSCONF in @c libx52/x52d_ipc.h. */
|
||||||
|
#define X52D_CONFIG_CLEAR_TARGET_SYSCONF X52D_IPC_CONFIG_CLEAR_TARGET_SYSCONF
|
||||||
|
|
||||||
#define X52D_PID_FILE RUNDIR "/" X52D_APP_NAME ".pid"
|
#define X52D_PID_FILE RUNDIR "/" X52D_APP_NAME ".pid"
|
||||||
|
|
||||||
#define X52D_SOCK_COMMAND RUNDIR "/" X52D_APP_NAME ".cmd"
|
#define X52D_SOCK_COMMAND RUNDIR "/" X52D_APP_NAME ".cmd"
|
||||||
#define X52D_SOCK_NOTIFY RUNDIR "/" X52D_APP_NAME ".notify"
|
#define X52D_SOCK_NOTIFY RUNDIR "/" X52D_APP_NAME ".notify"
|
||||||
|
|
||||||
|
/** Framed IPC socket: RPC commands and async pushes share one path (legacy NUL \c .cmd / \c .notify remain until removal). */
|
||||||
|
#define X52D_SOCK_IPC RUNDIR "/" X52D_APP_NAME ".socket"
|
||||||
|
|
||||||
#include "gettext.h"
|
#include "gettext.h"
|
||||||
#define N_(x) gettext_noop(x)
|
#define N_(x) gettext_noop(x)
|
||||||
#define _(x) gettext(x)
|
#define _(x) gettext(x)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,18 @@ the Windows X52 driver. It currently manages the following:
|
||||||
- MFD brightness
|
- MFD brightness
|
||||||
- Clock display on MFD
|
- Clock display on MFD
|
||||||
|
|
||||||
|
# Control and notification sockets
|
||||||
|
|
||||||
|
The \b supported control plane and event stream use \b liblocalipc framing on a
|
||||||
|
single UNIX stream socket (default basename \c x52d.socket under the runtime
|
||||||
|
directory). See \ref x52d_protocol and \ref proto_lipc_framed. Use \c -S to
|
||||||
|
override the socket path.
|
||||||
|
|
||||||
|
The historical \b NUL-separated command socket (\c x52d.cmd) and notify socket
|
||||||
|
(\c x52d.notify) remain for transitional callers; they are \b deprecated and
|
||||||
|
\b will be removed in a future release. Overrides use \c -s and \c -b. See
|
||||||
|
\ref x52d_protocol.
|
||||||
|
|
||||||
# Command line arguments
|
# Command line arguments
|
||||||
|
|
||||||
- \c -f - Run daemon in foreground (default: no)
|
- \c -f - Run daemon in foreground (default: no)
|
||||||
|
|
@ -18,8 +30,9 @@ the Windows X52 driver. It currently manages the following:
|
||||||
- \c -c - Path to configuration file
|
- \c -c - Path to configuration file
|
||||||
- \c -p - Path to PID file
|
- \c -p - Path to PID file
|
||||||
- \c -o - Configuration override - only applied during startup
|
- \c -o - Configuration override - only applied during startup
|
||||||
- \c -s - Path to command socket (see \ref x52d_protocol)
|
- \c -S - Path to the \b unified framed-IPC socket (RPC and \c tid==0 push notifications such as \c DEVICE_STATE; default \c x52d.socket under the runtime directory). \b This is the supported integration path (see \ref x52d_protocol).
|
||||||
- \c -b - Path to notify socket
|
- \c -s - Path to the \b deprecated legacy NUL command socket (default \c x52d.cmd); \b will be removed once callers migrate to \c -S / LIPC.
|
||||||
|
- \c -b - Path to the \b deprecated legacy notify socket (default \c x52d.notify); \b will be removed in the same way.
|
||||||
|
|
||||||
# Configuration file
|
# Configuration file
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,192 +0,0 @@
|
||||||
/*
|
|
||||||
* Saitek X52 Pro MFD & LED driver - Daemon controller
|
|
||||||
*
|
|
||||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@page x52ctl Command Line controller to X52 daemon
|
|
||||||
|
|
||||||
\htmlonly
|
|
||||||
<b>x52ctl</b> - Command line controller to X52 daemon
|
|
||||||
\endhtmlonly
|
|
||||||
|
|
||||||
# SYNOPSIS
|
|
||||||
<tt>\b x52ctl [\a -i] [\a -s socket-path] [command] </tt>
|
|
||||||
|
|
||||||
# DESCRIPTION
|
|
||||||
|
|
||||||
x52ctl is a program that can be used to communicate with the X52 daemon. It can
|
|
||||||
be used either as a one-shot program that can be run from another program or
|
|
||||||
script, or it can be run interactively.
|
|
||||||
|
|
||||||
Commands are sent to the running daemon, and responses are written to standard
|
|
||||||
output.
|
|
||||||
|
|
||||||
If not running interactively, then you must specify a command, or the program
|
|
||||||
will exit with a failure exit code. If running interactively, the program will
|
|
||||||
request input and send that to the daemon, until the user either enters the
|
|
||||||
string "quit", or terminates input by using Ctrl+D.
|
|
||||||
|
|
||||||
# OPTIONS
|
|
||||||
|
|
||||||
- <tt>\b -i</tt>
|
|
||||||
Run in interactive mode. Any additional non-option arguments are ignored.
|
|
||||||
|
|
||||||
- <tt>\b -s < \a socket-path ></tt>
|
|
||||||
Use the socket at the given path. If this is not specified, then it uses a
|
|
||||||
default socket.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "build-config.h"
|
|
||||||
#include <ctype.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <errno.h>
|
|
||||||
|
|
||||||
#include <daemon/constants.h>
|
|
||||||
#include <libx52/x52dcomm.h>
|
|
||||||
|
|
||||||
#define APP_NAME "x52ctl"
|
|
||||||
#if HAVE_FUNC_ATTRIBUTE_NORETURN
|
|
||||||
__attribute__((noreturn))
|
|
||||||
#endif
|
|
||||||
static void usage(int exit_code)
|
|
||||||
{
|
|
||||||
fprintf(stderr, _("Usage: %s [-i] [-s socket-path] [command]\n"), APP_NAME);
|
|
||||||
exit(exit_code);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int send_command(int sock_fd, int argc, char **argv)
|
|
||||||
{
|
|
||||||
int rc;
|
|
||||||
char buffer[1024];
|
|
||||||
int buflen;
|
|
||||||
|
|
||||||
buflen = x52d_format_command(argc, (const char **)argv, buffer, sizeof(buffer));
|
|
||||||
if (buflen < 0) {
|
|
||||||
if (errno == E2BIG) {
|
|
||||||
fprintf(stderr, _("Argument length too long\n"));
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
rc = x52d_send_command(sock_fd, buffer, buflen, sizeof(buffer));
|
|
||||||
if (rc >= 0) {
|
|
||||||
if (write(STDOUT_FILENO, buffer, rc) < 0) {
|
|
||||||
perror("write");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
perror("x52d_send_command");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void interactive_mode(int sock_fd)
|
|
||||||
{
|
|
||||||
bool keep_running = true;
|
|
||||||
char buffer[1024];
|
|
||||||
|
|
||||||
fputs("> ", stdout);
|
|
||||||
while (keep_running && fgets(buffer, sizeof(buffer), stdin) != NULL) {
|
|
||||||
int sargc;
|
|
||||||
char *sargv[512] = { 0 };
|
|
||||||
int pos;
|
|
||||||
|
|
||||||
if (strcasecmp(buffer, "quit\n") == 0) {
|
|
||||||
keep_running = false;
|
|
||||||
} else {
|
|
||||||
/* Break the buffer into argc/argv */
|
|
||||||
sargc = 0;
|
|
||||||
pos = 0;
|
|
||||||
while (buffer[pos]) {
|
|
||||||
if (isspace(buffer[pos])) {
|
|
||||||
buffer[pos] = '\0';
|
|
||||||
pos++;
|
|
||||||
} else {
|
|
||||||
sargv[sargc] = &buffer[pos];
|
|
||||||
sargc++;
|
|
||||||
for (; buffer[pos] && !isspace(buffer[pos]); pos++);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (send_command(sock_fd, sargc, sargv)) {
|
|
||||||
keep_running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keep_running) {
|
|
||||||
fputs("\n> ", stdout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
|
||||||
{
|
|
||||||
bool interactive = false;
|
|
||||||
const char *socket_path = NULL;
|
|
||||||
int opt;
|
|
||||||
int sock_fd;
|
|
||||||
int rc = EXIT_SUCCESS;
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Parse command line arguments
|
|
||||||
*
|
|
||||||
* -i Interactive
|
|
||||||
* -s Socket path
|
|
||||||
*/
|
|
||||||
while ((opt = getopt(argc, argv, "is:h")) != -1) {
|
|
||||||
switch (opt) {
|
|
||||||
case 'i':
|
|
||||||
interactive = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 's':
|
|
||||||
socket_path = optarg;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'h':
|
|
||||||
usage(EXIT_SUCCESS);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
usage(EXIT_FAILURE);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!interactive && optind >= argc) {
|
|
||||||
usage(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Connect to the socket */
|
|
||||||
sock_fd = x52d_dial_command(socket_path);
|
|
||||||
if (sock_fd < 0) {
|
|
||||||
perror("x52d_dial_command");
|
|
||||||
return EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interactive) {
|
|
||||||
if (optind < argc) {
|
|
||||||
fprintf(stderr,
|
|
||||||
_("Running in interactive mode, ignoring extra arguments\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
interactive_mode(sock_fd);
|
|
||||||
} else {
|
|
||||||
if (send_command(sock_fd, argc - optind, &argv[optind])) {
|
|
||||||
rc = EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close(sock_fd);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "build-config.h"
|
#include "build-config.h"
|
||||||
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
@ -50,8 +51,14 @@ static void *x52_dev_thr(void *param)
|
||||||
sleep(DEV_ACQ_DELAY);
|
sleep(DEV_ACQ_DELAY);
|
||||||
} else {
|
} else {
|
||||||
/* Successfully connected */
|
/* Successfully connected */
|
||||||
|
uint16_t vid = 0;
|
||||||
|
uint16_t pid = 0;
|
||||||
|
const char *prod = "";
|
||||||
|
|
||||||
PINELOG_INFO(_("Device connected, writing configuration"));
|
PINELOG_INFO(_("Device connected, writing configuration"));
|
||||||
X52D_NOTIFY("CONNECTED");
|
(void)libx52_get_usb_ids(x52_dev, &vid, &pid);
|
||||||
|
prod = libx52_get_product_string(x52_dev);
|
||||||
|
x52d_notify_device_state(1, vid, pid, prod, strlen(prod));
|
||||||
x52d_config_apply();
|
x52d_config_apply();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -171,11 +178,15 @@ int x52d_dev_update(void)
|
||||||
|
|
||||||
if (rc != LIBX52_SUCCESS) {
|
if (rc != LIBX52_SUCCESS) {
|
||||||
if (rc == LIBX52_ERROR_NO_DEVICE) {
|
if (rc == LIBX52_ERROR_NO_DEVICE) {
|
||||||
|
uint16_t vid = 0;
|
||||||
|
uint16_t pid = 0;
|
||||||
|
|
||||||
// Detach from the existing device, the next thread run will
|
// Detach from the existing device, the next thread run will
|
||||||
// pick it up.
|
// pick it up.
|
||||||
PINELOG_TRACE("Disconnecting detached device");
|
PINELOG_TRACE("Disconnecting detached device");
|
||||||
|
(void)libx52_get_usb_ids(x52_dev, &vid, &pid);
|
||||||
libx52_disconnect(x52_dev);
|
libx52_disconnect(x52_dev);
|
||||||
X52D_NOTIFY("DISCONNECTED");
|
x52d_notify_device_state(0, vid, pid, NULL, 0);
|
||||||
} else {
|
} else {
|
||||||
PINELOG_ERROR(_("Error %d when updating X52 device: %s"),
|
PINELOG_ERROR(_("Error %d when updating X52 device: %s"),
|
||||||
rc, libx52_strerror(rc));
|
rc, libx52_strerror(rc));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||||
|
"""Meson install_script helper: copy one file to DESTDIR + absolute install path."""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
raise SystemExit('usage: install_copy_file.py SRC DEST_ABS')
|
||||||
|
src = sys.argv[1]
|
||||||
|
dest_abs = sys.argv[2]
|
||||||
|
destdir = os.environ.get('DESTDIR', '')
|
||||||
|
if destdir:
|
||||||
|
dest = os.path.normpath(os.path.join(destdir, dest_abs.lstrip(os.sep)))
|
||||||
|
else:
|
||||||
|
dest = dest_abs
|
||||||
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||||
|
shutil.copyfile(src, dest)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,367 @@
|
||||||
|
/*
|
||||||
|
* Saitek X52 Pro MFD & LED driver - framed IPC opcode handlers
|
||||||
|
*
|
||||||
|
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "build-config.h"
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define PINELOG_MODULE X52D_MOD_CLIENT
|
||||||
|
#include "pinelog.h"
|
||||||
|
#include <daemon/config.h>
|
||||||
|
#include <daemon/constants.h>
|
||||||
|
#include <daemon/ipc_lipc_handlers.h>
|
||||||
|
#include <localipc/lipc.h>
|
||||||
|
|
||||||
|
#include "config-defs.h"
|
||||||
|
#include "module-map.h"
|
||||||
|
|
||||||
|
static lipc_status reply_err(lipc_server_reply_ctx *reply, lipc_status st, const char *msg)
|
||||||
|
{
|
||||||
|
size_t len = (msg != NULL) ? strlen(msg) : 0;
|
||||||
|
|
||||||
|
return lipc_server_reply(reply, (uint16_t)st, 0, 0, msg, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static lipc_status reply_ok(lipc_server_reply_ctx *reply, const void *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
return lipc_server_reply(reply, LIPC_OK, 0, 0, payload, payload_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static lipc_status on_config_load(void *user, lipc_server_reply_ctx *reply,
|
||||||
|
const lipc_header *req_hdr, const uint8_t *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
char *path;
|
||||||
|
int rc;
|
||||||
|
(void)user;
|
||||||
|
(void)req_hdr;
|
||||||
|
|
||||||
|
path = malloc(payload_len + 1u);
|
||||||
|
if (path == NULL) {
|
||||||
|
return reply_err(reply, LIPC_INTERNAL_ERROR, strerror(ENOMEM));
|
||||||
|
}
|
||||||
|
memcpy(path, payload, payload_len);
|
||||||
|
path[payload_len] = '\0';
|
||||||
|
|
||||||
|
rc = x52d_config_load_from_path(path);
|
||||||
|
free(path);
|
||||||
|
if (rc != 0) {
|
||||||
|
return reply_err(reply, LIPC_INVALID_PARAMS, strerror(rc));
|
||||||
|
}
|
||||||
|
x52d_config_apply();
|
||||||
|
return reply_ok(reply, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static lipc_status on_config_reload(void *user, lipc_server_reply_ctx *reply,
|
||||||
|
const lipc_header *req_hdr, const uint8_t *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
(void)user;
|
||||||
|
(void)req_hdr;
|
||||||
|
(void)payload;
|
||||||
|
(void)payload_len;
|
||||||
|
|
||||||
|
rc = x52d_config_reload_canonical();
|
||||||
|
if (rc != 0) {
|
||||||
|
return reply_err(reply, LIPC_INTERNAL_ERROR, strerror(rc));
|
||||||
|
}
|
||||||
|
x52d_config_apply();
|
||||||
|
return reply_ok(reply, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static lipc_status on_config_reset(void *user, lipc_server_reply_ctx *reply,
|
||||||
|
const lipc_header *req_hdr, const uint8_t *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
(void)user;
|
||||||
|
(void)req_hdr;
|
||||||
|
(void)payload;
|
||||||
|
(void)payload_len;
|
||||||
|
|
||||||
|
rc = x52d_config_reset_to_defaults();
|
||||||
|
if (rc != 0) {
|
||||||
|
return reply_err(reply, LIPC_INTERNAL_ERROR, strerror(rc));
|
||||||
|
}
|
||||||
|
x52d_config_apply();
|
||||||
|
return reply_ok(reply, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static lipc_status on_config_clear(void *user, lipc_server_reply_ctx *reply,
|
||||||
|
const lipc_header *req_hdr, const uint8_t *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
int unlink_err = 0;
|
||||||
|
(void)user;
|
||||||
|
(void)payload;
|
||||||
|
(void)payload_len;
|
||||||
|
|
||||||
|
if (req_hdr->index != X52D_CONFIG_CLEAR_TARGET_STATE
|
||||||
|
&& req_hdr->index != X52D_CONFIG_CLEAR_TARGET_SYSCONF) {
|
||||||
|
return reply_err(reply, LIPC_INVALID_PARAMS, "invalid CONFIG_CLEAR target index");
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = x52d_config_clear_disk_then_reload((uint16_t)req_hdr->index, &unlink_err);
|
||||||
|
if (rc != 0) {
|
||||||
|
return reply_err(reply, LIPC_INTERNAL_ERROR, strerror(rc));
|
||||||
|
}
|
||||||
|
x52d_config_apply();
|
||||||
|
if (unlink_err != 0) {
|
||||||
|
return reply_err(reply, LIPC_IO_ERROR, strerror(unlink_err));
|
||||||
|
}
|
||||||
|
return reply_ok(reply, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static lipc_status on_config_save(void *user, lipc_server_reply_ctx *reply,
|
||||||
|
const lipc_header *req_hdr, const uint8_t *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
(void)user;
|
||||||
|
(void)req_hdr;
|
||||||
|
(void)payload;
|
||||||
|
(void)payload_len;
|
||||||
|
|
||||||
|
rc = x52d_config_save_session();
|
||||||
|
if (rc != 0) {
|
||||||
|
return reply_err(reply, LIPC_IO_ERROR, strerror(rc));
|
||||||
|
}
|
||||||
|
return reply_ok(reply, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static lipc_status on_config_dump(void *user, lipc_server_reply_ctx *reply,
|
||||||
|
const lipc_header *req_hdr, const uint8_t *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
char *buf = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
int rc;
|
||||||
|
lipc_status st;
|
||||||
|
(void)user;
|
||||||
|
(void)req_hdr;
|
||||||
|
(void)payload;
|
||||||
|
(void)payload_len;
|
||||||
|
|
||||||
|
rc = x52d_config_dump_to_alloc(&buf, &len);
|
||||||
|
if (rc != 0) {
|
||||||
|
return reply_err(reply, LIPC_INTERNAL_ERROR, strerror(rc));
|
||||||
|
}
|
||||||
|
|
||||||
|
st = reply_ok(reply, buf, len);
|
||||||
|
free(buf);
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
static lipc_status on_config_set(void *user, lipc_server_reply_ctx *reply,
|
||||||
|
const lipc_header *req_hdr, const uint8_t *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
const char *sec_name;
|
||||||
|
const char *opt_name;
|
||||||
|
char *val_copy;
|
||||||
|
int rc;
|
||||||
|
uint16_t option_id;
|
||||||
|
(void)user;
|
||||||
|
|
||||||
|
if (req_hdr->value > UINT16_MAX) {
|
||||||
|
return reply_err(reply, LIPC_INVALID_PARAMS, "option id out of range");
|
||||||
|
}
|
||||||
|
option_id = (uint16_t)req_hdr->value;
|
||||||
|
if (!x52d_config_registry_pair_valid(req_hdr->index, option_id)) {
|
||||||
|
return reply_err(reply, LIPC_INVALID_PARAMS, "invalid section or option id");
|
||||||
|
}
|
||||||
|
|
||||||
|
sec_name = section_names[req_hdr->index];
|
||||||
|
opt_name = option_names[req_hdr->index][option_id];
|
||||||
|
if (sec_name == NULL || opt_name == NULL) {
|
||||||
|
return reply_err(reply, LIPC_INVALID_PARAMS, "invalid configuration key");
|
||||||
|
}
|
||||||
|
|
||||||
|
val_copy = malloc(payload_len + 1u);
|
||||||
|
if (val_copy == NULL) {
|
||||||
|
return reply_err(reply, LIPC_INTERNAL_ERROR, strerror(ENOMEM));
|
||||||
|
}
|
||||||
|
memcpy(val_copy, payload, payload_len);
|
||||||
|
val_copy[payload_len] = '\0';
|
||||||
|
|
||||||
|
rc = x52d_config_set(sec_name, opt_name, val_copy);
|
||||||
|
free(val_copy);
|
||||||
|
if (rc != 0) {
|
||||||
|
return reply_err(reply, LIPC_INVALID_PARAMS, strerror(rc));
|
||||||
|
}
|
||||||
|
x52d_config_apply_immediate(sec_name, opt_name);
|
||||||
|
return reply_ok(reply, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static lipc_status on_config_get(void *user, lipc_server_reply_ctx *reply,
|
||||||
|
const lipc_header *req_hdr, const uint8_t *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
const char *sec_name;
|
||||||
|
const char *opt_name;
|
||||||
|
const char *val;
|
||||||
|
uint16_t option_id;
|
||||||
|
(void)user;
|
||||||
|
(void)payload;
|
||||||
|
(void)payload_len;
|
||||||
|
|
||||||
|
if (req_hdr->value > UINT16_MAX) {
|
||||||
|
return reply_err(reply, LIPC_INVALID_PARAMS, "option id out of range");
|
||||||
|
}
|
||||||
|
option_id = (uint16_t)req_hdr->value;
|
||||||
|
if (!x52d_config_registry_pair_valid(req_hdr->index, option_id)) {
|
||||||
|
return reply_err(reply, LIPC_INVALID_PARAMS, "invalid section or option id");
|
||||||
|
}
|
||||||
|
|
||||||
|
sec_name = section_names[req_hdr->index];
|
||||||
|
opt_name = option_names[req_hdr->index][option_id];
|
||||||
|
val = x52d_config_get(sec_name, opt_name);
|
||||||
|
if (val == NULL) {
|
||||||
|
return reply_err(reply, LIPC_NOT_FOUND, "configuration key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply_ok(reply, val, strlen(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
static lipc_status on_logging_show(void *user, lipc_server_reply_ctx *reply,
|
||||||
|
const lipc_header *req_hdr, const uint8_t *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
int level;
|
||||||
|
const char *label;
|
||||||
|
(void)user;
|
||||||
|
(void)payload;
|
||||||
|
(void)payload_len;
|
||||||
|
|
||||||
|
if (!x52d_module_wire_valid(req_hdr->index)) {
|
||||||
|
return reply_err(reply, LIPC_INVALID_PARAMS, "invalid module id");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req_hdr->index == X52D_MOD_GLOBAL) {
|
||||||
|
level = pinelog_get_level();
|
||||||
|
} else {
|
||||||
|
level = pinelog_get_module_level((int)req_hdr->index);
|
||||||
|
}
|
||||||
|
|
||||||
|
label = lookup_level_by_id(level);
|
||||||
|
if (label == NULL) {
|
||||||
|
return reply_err(reply, LIPC_INTERNAL_ERROR, "unknown log level in effect");
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply_ok(reply, label, strlen(label));
|
||||||
|
}
|
||||||
|
|
||||||
|
static lipc_status on_logging_set(void *user, lipc_server_reply_ctx *reply,
|
||||||
|
const lipc_header *req_hdr, const uint8_t *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
int64_t wire = (int64_t)req_hdr->value;
|
||||||
|
(void)user;
|
||||||
|
(void)payload;
|
||||||
|
(void)payload_len;
|
||||||
|
|
||||||
|
if (!x52d_module_wire_valid(req_hdr->index)) {
|
||||||
|
return reply_err(reply, LIPC_INVALID_PARAMS, "invalid module id");
|
||||||
|
}
|
||||||
|
if (!x52d_log_level_wire_valid(req_hdr->value)) {
|
||||||
|
return reply_err(reply, LIPC_INVALID_PARAMS, "invalid log level id");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req_hdr->index == X52D_MOD_GLOBAL) {
|
||||||
|
pinelog_set_level((int)wire);
|
||||||
|
} else {
|
||||||
|
pinelog_set_module_level((int)req_hdr->index, (int)wire);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply_ok(reply, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const lipc_method_desc desc_config_load = {
|
||||||
|
.index = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.value = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.payload = LIPC_FIELD_REQUIRED,
|
||||||
|
.payload_min_len = 1,
|
||||||
|
.payload_max_len = PATH_MAX - 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const lipc_method_desc desc_config_void = {
|
||||||
|
.index = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.value = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.payload = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.payload_min_len = 0,
|
||||||
|
.payload_max_len = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const lipc_method_desc desc_config_clear = {
|
||||||
|
.index = LIPC_FIELD_REQUIRED,
|
||||||
|
.value = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.payload = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.payload_min_len = 0,
|
||||||
|
.payload_max_len = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const lipc_method_desc desc_config_dump = {
|
||||||
|
.index = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.value = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.payload = LIPC_FIELD_OPTIONAL,
|
||||||
|
.payload_min_len = 0,
|
||||||
|
.payload_max_len = UINT32_MAX,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const lipc_method_desc desc_config_set = {
|
||||||
|
.index = LIPC_FIELD_REQUIRED,
|
||||||
|
.value = LIPC_FIELD_REQUIRED,
|
||||||
|
.payload = LIPC_FIELD_REQUIRED,
|
||||||
|
.payload_min_len = 1,
|
||||||
|
.payload_max_len = NAME_MAX,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const lipc_method_desc desc_config_get = {
|
||||||
|
.index = LIPC_FIELD_REQUIRED,
|
||||||
|
.value = LIPC_FIELD_REQUIRED,
|
||||||
|
.payload = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.payload_min_len = 0,
|
||||||
|
.payload_max_len = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const lipc_method_desc desc_logging_show = {
|
||||||
|
/* Module id 0 is valid (e.g. CONFIG); liblocalipc REQUIRED would incorrectly reject index==0. */
|
||||||
|
.index = LIPC_FIELD_OPTIONAL,
|
||||||
|
/* SHOW reads level from pinelog only; @c value must be zero on the wire. */
|
||||||
|
.value = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.payload = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.payload_min_len = 0,
|
||||||
|
.payload_max_len = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const lipc_method_desc desc_logging_set = {
|
||||||
|
.index = LIPC_FIELD_OPTIONAL,
|
||||||
|
/* FATAL is level id 0; REQUIRED would reject it — validate with @c x52d_log_level_wire_valid. */
|
||||||
|
.value = LIPC_FIELD_OPTIONAL,
|
||||||
|
.payload = LIPC_FIELD_FORBIDDEN,
|
||||||
|
.payload_min_len = 0,
|
||||||
|
.payload_max_len = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const lipc_server_handler x52d_ipc_handlers[] = {
|
||||||
|
{ X52D_IPC_CONFIG_LOAD, &desc_config_load, on_config_load },
|
||||||
|
{ X52D_IPC_CONFIG_RELOAD, &desc_config_void, on_config_reload },
|
||||||
|
{ X52D_IPC_CONFIG_RESET, &desc_config_void, on_config_reset },
|
||||||
|
{ X52D_IPC_CONFIG_CLEAR, &desc_config_clear, on_config_clear },
|
||||||
|
{ X52D_IPC_CONFIG_SAVE, &desc_config_void, on_config_save },
|
||||||
|
{ X52D_IPC_CONFIG_DUMP, &desc_config_dump, on_config_dump },
|
||||||
|
{ X52D_IPC_CONFIG_SET, &desc_config_set, on_config_set },
|
||||||
|
{ X52D_IPC_CONFIG_GET, &desc_config_get, on_config_get },
|
||||||
|
{ X52D_IPC_LOGGING_SHOW, &desc_logging_show, on_logging_show },
|
||||||
|
{ X52D_IPC_LOGGING_SET, &desc_logging_set, on_logging_set },
|
||||||
|
};
|
||||||
|
|
||||||
|
const lipc_server_handler *x52d_ipc_handlers_table(void)
|
||||||
|
{
|
||||||
|
return x52d_ipc_handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t x52d_ipc_handlers_count(void)
|
||||||
|
{
|
||||||
|
return sizeof(x52d_ipc_handlers) / sizeof(x52d_ipc_handlers[0]);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Saitek X52 Pro MFD & LED driver - framed IPC opcode handlers
|
||||||
|
*
|
||||||
|
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef X52D_IPC_LIPC_HANDLERS_H
|
||||||
|
#define X52D_IPC_LIPC_HANDLERS_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <libx52/x52d_ipc.h>
|
||||||
|
#include <localipc/lipc.h>
|
||||||
|
|
||||||
|
const lipc_server_handler *x52d_ipc_handlers_table(void);
|
||||||
|
size_t x52d_ipc_handlers_count(void);
|
||||||
|
|
||||||
|
#endif /* !defined X52D_IPC_LIPC_HANDLERS_H */
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* Saitek X52 Pro MFD & LED driver - framed IPC listener (unified command + notify)
|
||||||
|
*
|
||||||
|
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "build-config.h"
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define PINELOG_MODULE X52D_MOD_CLIENT
|
||||||
|
#include "pinelog.h"
|
||||||
|
#include <daemon/constants.h>
|
||||||
|
#include <daemon/ipc_lipc_handlers.h>
|
||||||
|
#include <daemon/ipc_service.h>
|
||||||
|
#include <daemon/x52dcomm-internal.h>
|
||||||
|
#include <libx52/x52d_ipc.h>
|
||||||
|
#include <localipc/lipc.h>
|
||||||
|
|
||||||
|
static lipc_server *ipc_srv_ctx;
|
||||||
|
static pthread_t ipc_listen_thread;
|
||||||
|
static const char *ipc_path_stored;
|
||||||
|
static bool ipc_listen_thread_started;
|
||||||
|
|
||||||
|
static void *ipc_listen_thread_main(void *arg)
|
||||||
|
{
|
||||||
|
lipc_server *srv = (lipc_server *)arg;
|
||||||
|
(void)lipc_server_run(srv);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ipc_stop_join(pthread_t *thr, bool *started, lipc_server **ctx,
|
||||||
|
const char *path_for_unlink)
|
||||||
|
{
|
||||||
|
if (*ctx != NULL) {
|
||||||
|
(void)lipc_server_stop(*ctx);
|
||||||
|
}
|
||||||
|
if (*started) {
|
||||||
|
(void)pthread_join(*thr, NULL);
|
||||||
|
*started = false;
|
||||||
|
}
|
||||||
|
if (*ctx != NULL) {
|
||||||
|
lipc_server_destroy(*ctx);
|
||||||
|
*ctx = NULL;
|
||||||
|
}
|
||||||
|
if (path_for_unlink != NULL && path_for_unlink[0] != '\0') {
|
||||||
|
(void)unlink(path_for_unlink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void x52d_ipc_exit(void)
|
||||||
|
{
|
||||||
|
ipc_stop_join(&ipc_listen_thread, &ipc_listen_thread_started, &ipc_srv_ctx, ipc_path_stored);
|
||||||
|
ipc_path_stored = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void x52d_ipc_push_device_state(uint16_t index, uint64_t value, const void *payload,
|
||||||
|
size_t payload_len)
|
||||||
|
{
|
||||||
|
lipc_server *srv = ipc_srv_ctx;
|
||||||
|
|
||||||
|
if (srv == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index > 1u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(void)lipc_server_broadcast_notify(srv, X52D_IPC_PUSH_DEVICE_STATE, (uint16_t)LIPC_OK, index,
|
||||||
|
value, payload, payload_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
int x52d_ipc_init(const char *sock_path)
|
||||||
|
{
|
||||||
|
const char *path;
|
||||||
|
int listen_fd;
|
||||||
|
lipc_server *srv;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
ipc_srv_ctx = NULL;
|
||||||
|
ipc_listen_thread_started = false;
|
||||||
|
ipc_path_stored = NULL;
|
||||||
|
|
||||||
|
path = x52d_ipc_sock_path(sock_path);
|
||||||
|
|
||||||
|
listen_fd = lipc_socket_listen(path, X52D_MAX_CLIENTS, LIPC_SOCKET_NONBLOCK);
|
||||||
|
if (listen_fd < 0) {
|
||||||
|
PINELOG_ERROR(_("Error listening on framed IPC socket %s: %s"), path, strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
srv = lipc_server_create(listen_fd, LIPC_MAX_PAYLOAD_DEFAULT,
|
||||||
|
x52d_ipc_handlers_table(), x52d_ipc_handlers_count(), NULL);
|
||||||
|
if (srv == NULL) {
|
||||||
|
PINELOG_ERROR(_("Error creating framed IPC server for %s: %s"), path, strerror(errno));
|
||||||
|
close(listen_fd);
|
||||||
|
unlink(path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipc_path_stored = path;
|
||||||
|
ipc_srv_ctx = srv;
|
||||||
|
rc = pthread_create(&ipc_listen_thread, NULL, ipc_listen_thread_main, srv);
|
||||||
|
if (rc != 0) {
|
||||||
|
PINELOG_ERROR(_("Error %d starting framed IPC thread for %s: %s"), rc, path, strerror(rc));
|
||||||
|
lipc_server_destroy(srv);
|
||||||
|
ipc_srv_ctx = NULL;
|
||||||
|
unlink(path);
|
||||||
|
ipc_path_stored = NULL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipc_listen_thread_started = true;
|
||||||
|
PINELOG_INFO(_("Framed IPC server listening on %s"), path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Saitek X52 Pro MFD & LED driver - framed IPC listener (unified command + notify)
|
||||||
|
*
|
||||||
|
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef X52D_IPC_SERVICE_H
|
||||||
|
#define X52D_IPC_SERVICE_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the framed-IPC server on the given path (NULL = default from constants.h).
|
||||||
|
* One UNIX socket serves both RPC-style requests and server-initiated pushes.
|
||||||
|
*
|
||||||
|
* @return 0 on success, -1 on failure (errno set where applicable).
|
||||||
|
*/
|
||||||
|
int x52d_ipc_init(const char *sock_path);
|
||||||
|
|
||||||
|
/** Stop the listener (wake + join + close) and remove the bound socket path. */
|
||||||
|
void x52d_ipc_exit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast DEVICE_STATE (\c X52D_IPC_PUSH_DEVICE_STATE) to all framed-IPC clients.
|
||||||
|
* No-op when the IPC server is not running. @p index: 0 = disconnected, 1 = connected;
|
||||||
|
* @p value lower 32 bits encode \c (vid<<16)|pid per @ref x52d_ipc_device_state_pack_usb.
|
||||||
|
*/
|
||||||
|
void x52d_ipc_push_device_state(uint16_t index, uint64_t value, const void *payload,
|
||||||
|
size_t payload_len);
|
||||||
|
|
||||||
|
#endif /* !defined(X52D_IPC_SERVICE_H) */
|
||||||
|
|
@ -25,9 +25,9 @@
|
||||||
#include <daemon/mouse.h>
|
#include <daemon/mouse.h>
|
||||||
#include <daemon/command.h>
|
#include <daemon/command.h>
|
||||||
#include <daemon/notify.h>
|
#include <daemon/notify.h>
|
||||||
|
#include <daemon/ipc_service.h>
|
||||||
#include <daemon/x52dcomm-internal.h>
|
#include <daemon/x52dcomm-internal.h>
|
||||||
#include <daemon/keyboard_layout.h>
|
#include <daemon/keyboard_layout.h>
|
||||||
#include <libx52/x52dcomm.h>
|
|
||||||
#include "pinelog.h"
|
#include "pinelog.h"
|
||||||
|
|
||||||
static volatile int flag_quit;
|
static volatile int flag_quit;
|
||||||
|
|
@ -96,7 +96,8 @@ static void usage(int exit_code)
|
||||||
"\t[-l log-file] [-o override]\n"
|
"\t[-l log-file] [-o override]\n"
|
||||||
"\t[-c config-file] [-p pid-file]\n"
|
"\t[-c config-file] [-p pid-file]\n"
|
||||||
"\t[-s command-socket-path]\n"
|
"\t[-s command-socket-path]\n"
|
||||||
"\t[-b notify-socket-path]\n"),
|
"\t[-b notify-socket-path]\n"
|
||||||
|
"\t[-S framed-ipc-socket-path]\n"),
|
||||||
X52D_APP_NAME);
|
X52D_APP_NAME);
|
||||||
exit(exit_code);
|
exit(exit_code);
|
||||||
}
|
}
|
||||||
|
|
@ -214,6 +215,7 @@ int main(int argc, char **argv)
|
||||||
const char *pid_file = NULL;
|
const char *pid_file = NULL;
|
||||||
const char *command_sock = NULL;
|
const char *command_sock = NULL;
|
||||||
const char *notify_sock = NULL;
|
const char *notify_sock = NULL;
|
||||||
|
const char *ipc_sock = NULL;
|
||||||
int opt;
|
int opt;
|
||||||
int rc;
|
int rc;
|
||||||
sigset_t sigblockset;
|
sigset_t sigblockset;
|
||||||
|
|
@ -241,8 +243,9 @@ int main(int argc, char **argv)
|
||||||
* -p path to PID file (only used if running in background)
|
* -p path to PID file (only used if running in background)
|
||||||
* -s path to command socket
|
* -s path to command socket
|
||||||
* -b path to notify socket
|
* -b path to notify socket
|
||||||
|
* -S path to framed IPC socket (commands and notifications)
|
||||||
*/
|
*/
|
||||||
while ((opt = getopt(argc, argv, "fvql:o:c:p:s:b:h")) != -1) {
|
while ((opt = getopt(argc, argv, "fvql:o:c:p:s:b:S:h")) != -1) {
|
||||||
switch (opt) {
|
switch (opt) {
|
||||||
case 'f':
|
case 'f':
|
||||||
foreground = true;
|
foreground = true;
|
||||||
|
|
@ -291,6 +294,10 @@ int main(int argc, char **argv)
|
||||||
notify_sock = optarg;
|
notify_sock = optarg;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'S':
|
||||||
|
ipc_sock = optarg;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'h':
|
case 'h':
|
||||||
usage(EXIT_SUCCESS);
|
usage(EXIT_SUCCESS);
|
||||||
break;
|
break;
|
||||||
|
|
@ -301,6 +308,8 @@ int main(int argc, char **argv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x52d_config_set_ipc_save_path(conf_file);
|
||||||
|
|
||||||
PINELOG_DEBUG(_("Foreground = %s"), foreground ? _("true") : _("false"));
|
PINELOG_DEBUG(_("Foreground = %s"), foreground ? _("true") : _("false"));
|
||||||
PINELOG_DEBUG(_("Quiet = %s"), quiet ? _("true") : _("false"));
|
PINELOG_DEBUG(_("Quiet = %s"), quiet ? _("true") : _("false"));
|
||||||
PINELOG_DEBUG(_("Verbosity = %d"), verbosity);
|
PINELOG_DEBUG(_("Verbosity = %d"), verbosity);
|
||||||
|
|
@ -309,6 +318,7 @@ int main(int argc, char **argv)
|
||||||
PINELOG_DEBUG(_("PID file = %s"), pid_file);
|
PINELOG_DEBUG(_("PID file = %s"), pid_file);
|
||||||
PINELOG_DEBUG(_("Command socket = %s"), command_sock);
|
PINELOG_DEBUG(_("Command socket = %s"), command_sock);
|
||||||
PINELOG_DEBUG(_("Notify socket = %s"), notify_sock);
|
PINELOG_DEBUG(_("Notify socket = %s"), notify_sock);
|
||||||
|
PINELOG_DEBUG(_("Framed IPC socket = %s"), ipc_sock);
|
||||||
|
|
||||||
start_daemon(foreground, pid_file);
|
start_daemon(foreground, pid_file);
|
||||||
|
|
||||||
|
|
@ -330,6 +340,9 @@ int main(int argc, char **argv)
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
x52d_notify_init(notify_sock);
|
x52d_notify_init(notify_sock);
|
||||||
|
if (x52d_ipc_init(ipc_sock) < 0) {
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
x52d_io_init();
|
x52d_io_init();
|
||||||
x52d_mouse_handler_init();
|
x52d_mouse_handler_init();
|
||||||
|
|
||||||
|
|
@ -350,14 +363,22 @@ int main(int argc, char **argv)
|
||||||
/* Check if we need to reload configuration */
|
/* Check if we need to reload configuration */
|
||||||
if (flag_reload) {
|
if (flag_reload) {
|
||||||
PINELOG_INFO(_("Reloading X52 configuration"));
|
PINELOG_INFO(_("Reloading X52 configuration"));
|
||||||
x52d_config_load(conf_file);
|
if (x52d_config_reload_canonical() != 0) {
|
||||||
|
PINELOG_ERROR(_("Canonical configuration reload failed"));
|
||||||
|
}
|
||||||
x52d_config_apply();
|
x52d_config_apply();
|
||||||
flag_reload = false;
|
flag_reload = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flag_save_cfg) {
|
if (flag_save_cfg) {
|
||||||
PINELOG_INFO(_("Saving X52 configuration to disk"));
|
PINELOG_INFO(_("Saving X52 configuration to disk"));
|
||||||
x52d_config_save(conf_file);
|
{
|
||||||
|
int save_rc = x52d_config_save_session();
|
||||||
|
|
||||||
|
if (save_rc != 0) {
|
||||||
|
PINELOG_ERROR(_("Error saving configuration: %s"), strerror(save_rc));
|
||||||
|
}
|
||||||
|
}
|
||||||
flag_save_cfg = false;
|
flag_save_cfg = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -369,6 +390,7 @@ cleanup:
|
||||||
// Stop device threads
|
// Stop device threads
|
||||||
x52d_clock_exit();
|
x52d_clock_exit();
|
||||||
x52d_dev_exit();
|
x52d_dev_exit();
|
||||||
|
x52d_ipc_exit();
|
||||||
x52d_command_exit();
|
x52d_command_exit();
|
||||||
x52d_notify_exit();
|
x52d_notify_exit();
|
||||||
x52d_mouse_handler_exit();
|
x52d_mouse_handler_exit();
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ module_defs = custom_target('module-defs',
|
||||||
command: [python, meson.current_source_dir() / 'x52d_gen_module.py',
|
command: [python, meson.current_source_dir() / 'x52d_gen_module.py',
|
||||||
'@OUTPUT0@', '@OUTPUT1@'])
|
'@OUTPUT0@', '@OUTPUT1@'])
|
||||||
|
|
||||||
|
# Header for generated config section/option ids (build tree).
|
||||||
|
dep_config_defs_gen = declare_dependency(sources: config_defs[0])
|
||||||
|
|
||||||
# Header only: ordering on module-map.h without compiling module-map.c per target.
|
# Header only: ordering on module-map.h without compiling module-map.c per target.
|
||||||
dep_module_map_gen = declare_dependency(sources: module_defs[0])
|
dep_module_map_gen = declare_dependency(sources: module_defs[0])
|
||||||
|
|
||||||
|
|
@ -32,6 +35,8 @@ slib_comm_defs = static_library('x52dcommdefs',
|
||||||
'name-id-map.c',
|
'name-id-map.c',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
dep_threads = dependency('threads')
|
||||||
|
|
||||||
libx52dcomm_version = '1.0.0'
|
libx52dcomm_version = '1.0.0'
|
||||||
|
|
||||||
libx52dcomm_sources = [
|
libx52dcomm_sources = [
|
||||||
|
|
@ -42,7 +47,7 @@ libx52dcomm_sources = [
|
||||||
root_includes = include_directories('..')
|
root_includes = include_directories('..')
|
||||||
|
|
||||||
lib_libx52dcomm = library('x52dcomm', libx52dcomm_sources,
|
lib_libx52dcomm = library('x52dcomm', libx52dcomm_sources,
|
||||||
dependencies: [dep_intl, dep_config_h, dep_module_map_gen],
|
dependencies: [dep_intl, dep_config_h, dep_module_map_gen, dep_localipc, dep_threads],
|
||||||
version: libx52dcomm_version,
|
version: libx52dcomm_version,
|
||||||
c_args: sym_hidden_cargs,
|
c_args: sym_hidden_cargs,
|
||||||
install: true,
|
install: true,
|
||||||
|
|
@ -52,10 +57,13 @@ pkgconfig.generate(lib_libx52dcomm,
|
||||||
name: 'x52dcomm',
|
name: 'x52dcomm',
|
||||||
description: 'Client library for communicating with the x52d X52 daemon.',
|
description: 'Client library for communicating with the x52d X52 daemon.',
|
||||||
version: libx52dcomm_version,
|
version: libx52dcomm_version,
|
||||||
|
requires: ['localipc'],
|
||||||
)
|
)
|
||||||
|
|
||||||
x52d_sources = [
|
x52d_sources = [
|
||||||
'main.c',
|
'main.c',
|
||||||
|
'ipc_service.c',
|
||||||
|
'ipc_lipc_handlers.c',
|
||||||
'config_parser.c',
|
'config_parser.c',
|
||||||
'config_dump.c',
|
'config_dump.c',
|
||||||
'config.c',
|
'config.c',
|
||||||
|
|
@ -74,12 +82,10 @@ x52d_sources = [
|
||||||
'crc32.c',
|
'crc32.c',
|
||||||
]
|
]
|
||||||
|
|
||||||
dep_threads = dependency('threads')
|
# Comm sources are compiled into x52d (same as Autotools); libx52dcomm is for external clients and x52ctl (Python + ctypes).
|
||||||
|
|
||||||
# Comm sources are compiled into x52d (same as Autotools); libx52dcomm is only for x52ctl.
|
|
||||||
x52d_linkwith = [lib_libx52, lib_vkm, lib_libx52io, slib_comm_defs]
|
x52d_linkwith = [lib_libx52, lib_vkm, lib_libx52io, slib_comm_defs]
|
||||||
x52d_deps = [dep_pinelog, dep_inih, dep_threads, dep_math, dep_intl, dep_config_h,
|
x52d_deps = [dep_pinelog, dep_inih, dep_threads, dep_math, dep_intl, dep_config_h,
|
||||||
dep_module_map_gen]
|
dep_module_map_gen, dep_config_defs_gen, dep_localipc]
|
||||||
x52d_cflags = []
|
x52d_cflags = []
|
||||||
|
|
||||||
exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources,
|
exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources,
|
||||||
|
|
@ -89,11 +95,58 @@ exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources,
|
||||||
dependencies: x52d_deps,
|
dependencies: x52d_deps,
|
||||||
link_with: x52d_linkwith)
|
link_with: x52d_linkwith)
|
||||||
|
|
||||||
exe_x52ctl = executable('x52ctl', 'daemon_control.c',
|
x52ctl_script = configure_file(
|
||||||
install: true,
|
input: 'x52ctl.in',
|
||||||
dependencies: [dep_intl, dep_config_h, dep_module_map_gen],
|
output: 'x52ctl',
|
||||||
include_directories: [includes, root_includes],
|
configuration: {
|
||||||
link_with: lib_libx52dcomm)
|
'PYTHON': python.full_path(),
|
||||||
|
'X52D_PY_PKG': meson.current_build_dir(),
|
||||||
|
},
|
||||||
|
install: false,
|
||||||
|
)
|
||||||
|
|
||||||
|
x52d_python_install_dir = join_paths(get_option('datadir'), 'x52d', 'python')
|
||||||
|
x52ctl_pymain_abs = join_paths(get_option('prefix'), x52d_python_install_dir, 'x52ctl_main.py')
|
||||||
|
|
||||||
|
x52ctl_installed_sh = configure_file(
|
||||||
|
input: 'x52ctl-installed.in',
|
||||||
|
output: 'x52ctl.inst',
|
||||||
|
configuration: {
|
||||||
|
'PYTHON': '/usr/bin/env python3',
|
||||||
|
'X52CTL_PYMAIN': x52ctl_pymain_abs,
|
||||||
|
},
|
||||||
|
install: false,
|
||||||
|
)
|
||||||
|
|
||||||
|
x52ctl_buildpath = join_paths(meson.current_build_dir(), 'x52ctl')
|
||||||
|
x52ctl_test_dep = custom_target(
|
||||||
|
'x52ctl-test-dep',
|
||||||
|
command: [
|
||||||
|
find_program('sh'), '-c', 'chmod 755 "$1" && touch "$2"',
|
||||||
|
'_', x52ctl_buildpath, '@OUTPUT@',
|
||||||
|
],
|
||||||
|
output: 'x52ctl-test.stamp',
|
||||||
|
depend_files: [x52ctl_script],
|
||||||
|
)
|
||||||
|
|
||||||
|
install_data(
|
||||||
|
x52ctl_installed_sh,
|
||||||
|
install_dir: get_option('bindir'),
|
||||||
|
install_mode: 'rwxr-xr-x',
|
||||||
|
rename: 'x52ctl',
|
||||||
|
)
|
||||||
|
|
||||||
|
install_data(
|
||||||
|
files('command_defs.py', 'module_defs.py', 'x52ctl_main.py'),
|
||||||
|
install_dir: x52d_python_install_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
meson.add_install_script(
|
||||||
|
python,
|
||||||
|
files('install_copy_file.py'),
|
||||||
|
join_paths(meson.current_build_dir(), 'config_defs.py'),
|
||||||
|
join_paths(get_option('prefix'), x52d_python_install_dir, 'config_defs.py'),
|
||||||
|
)
|
||||||
|
|
||||||
install_data('x52d.conf',
|
install_data('x52d.conf',
|
||||||
install_dir: join_paths(get_option('sysconfdir'), 'x52d'))
|
install_dir: join_paths(get_option('sysconfdir'), 'x52d'))
|
||||||
|
|
@ -112,7 +165,7 @@ us_x52l = custom_target(
|
||||||
install_dir: join_paths(get_option('datadir'), 'x52d'))
|
install_dir: join_paths(get_option('datadir'), 'x52d'))
|
||||||
|
|
||||||
test('daemon-communication', files('test_daemon_comm.py')[0],
|
test('daemon-communication', files('test_daemon_comm.py')[0],
|
||||||
depends: [exe_x52d, exe_x52ctl], protocol: 'tap')
|
depends: [exe_x52d, config_defs, x52ctl_test_dep], protocol: 'tap')
|
||||||
|
|
||||||
x52d_mouse_test_sources = ['mouse_test.c', 'mouse.c']
|
x52d_mouse_test_sources = ['mouse_test.c', 'mouse.c']
|
||||||
x52d_mouse_test = executable('x52d-mouse-test', x52d_mouse_test_sources,
|
x52d_mouse_test = executable('x52d-mouse-test', x52d_mouse_test_sources,
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,13 @@
|
||||||
#define PINELOG_MODULE X52D_MOD_NOTIFY
|
#define PINELOG_MODULE X52D_MOD_NOTIFY
|
||||||
#include "pinelog.h"
|
#include "pinelog.h"
|
||||||
#include <daemon/constants.h>
|
#include <daemon/constants.h>
|
||||||
|
#include <daemon/ipc_service.h>
|
||||||
#include <daemon/notify.h>
|
#include <daemon/notify.h>
|
||||||
#include <daemon/client.h>
|
#include <daemon/client.h>
|
||||||
|
#include <libx52/x52d_ipc.h>
|
||||||
|
#define X52DCOMM_NO_DEPRECATED_ATTR 1
|
||||||
#include <libx52/x52dcomm.h>
|
#include <libx52/x52dcomm.h>
|
||||||
|
#undef X52DCOMM_NO_DEPRECATED_ATTR
|
||||||
#include <daemon/x52dcomm-internal.h>
|
#include <daemon/x52dcomm-internal.h>
|
||||||
|
|
||||||
static pthread_t notify_thr;
|
static pthread_t notify_thr;
|
||||||
|
|
@ -107,6 +111,23 @@ static void * x52_notify_thr(void * param)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void x52d_notify_device_state(int connected, uint16_t vid, uint16_t pid, const char *product_utf8,
|
||||||
|
size_t product_len)
|
||||||
|
{
|
||||||
|
uint64_t val = x52d_ipc_device_state_pack_usb(vid, pid);
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
X52D_NOTIFY("CONNECTED");
|
||||||
|
} else {
|
||||||
|
X52D_NOTIFY("DISCONNECTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (product_utf8 == NULL) {
|
||||||
|
product_len = 0;
|
||||||
|
}
|
||||||
|
x52d_ipc_push_device_state(connected ? 1u : 0u, val, product_utf8, product_len);
|
||||||
|
}
|
||||||
|
|
||||||
void x52d_notify_send(int argc, const char **argv)
|
void x52d_notify_send(int argc, const char **argv)
|
||||||
{
|
{
|
||||||
char buffer[X52D_BUFSZ + sizeof(uint16_t)];
|
char buffer[X52D_BUFSZ + sizeof(uint16_t)];
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,16 @@ void x52d_notify_init(const char *notify_sock_path);
|
||||||
void x52d_notify_exit(void);
|
void x52d_notify_exit(void);
|
||||||
void x52d_notify_send(int argc, const char **argv);
|
void x52d_notify_send(int argc, const char **argv);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit legacy NUL notify plus framed IPC DEVICE_STATE.
|
||||||
|
* @param connected Non-zero when the device is connected, zero on disconnect.
|
||||||
|
* @param vid @c idVendor (meaningful when @p connected; on disconnect, pass last device ids).
|
||||||
|
* @param pid @c idProduct
|
||||||
|
* @param product_utf8 Optional product name (connect only); may be NULL when @p product_len is 0.
|
||||||
|
*/
|
||||||
|
void x52d_notify_device_state(int connected, uint16_t vid, uint16_t pid, const char *product_utf8,
|
||||||
|
size_t product_len);
|
||||||
|
|
||||||
#define X52D_NOTIFY(...) do { \
|
#define X52D_NOTIFY(...) do { \
|
||||||
const char *argv ## __LINE__ [] = {__VA_ARGS__}; \
|
const char *argv ## __LINE__ [] = {__VA_ARGS__}; \
|
||||||
x52d_notify_send(sizeof(argv ## __LINE__ )/sizeof(argv ## __LINE__ [0]), argv ## __LINE__ ); \
|
x52d_notify_send(sizeof(argv ## __LINE__ )/sizeof(argv ## __LINE__ [0]), argv ## __LINE__ ); \
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,56 @@
|
||||||
/**
|
/**
|
||||||
@page x52d_protocol X52 daemon socket communication protocol
|
@page x52d_protocol X52 daemon control and notification protocols
|
||||||
|
|
||||||
The X52 daemon creates a Unix domain stream socket, by default at
|
@section proto_supported Primary path: framed LIPC (liblocalipc)
|
||||||
`$(LOCALSTATEDIR)/run/x52d.cmd` and listens for connection requests from
|
|
||||||
clients at this location. This can be overridden by passing the -s flag when
|
|
||||||
starting the daemon.
|
|
||||||
|
|
||||||
# Protocol Overview
|
The **supported** integration surface for daemon control and for **push**
|
||||||
|
notifications is **liblocalipc** on a single UNIX **stream** socket. By default
|
||||||
|
the daemon listens at `$(LOCALSTATEDIR)/run/x52d.socket` (same layout as other
|
||||||
|
runtime files under the configured runtime directory). The path is overridden
|
||||||
|
with the daemon’s \c -S option.
|
||||||
|
|
||||||
\b x52d requires that clients send it commands as a series of NUL terminated
|
Clients should use \ref x52d_dial_ipc and \ref x52d_ipc_call from
|
||||||
|
\ref x52dcomm.h (and opcode / field definitions in \ref x52d_ipc.h). RPC
|
||||||
|
replies carry a non-zero transaction id (\c lipc_header.tid) matching the
|
||||||
|
request. **Server push** frames use \c tid == \c 0; see \ref proto_lipc_framed
|
||||||
|
and especially \ref lipc_push_device_state.
|
||||||
|
|
||||||
|
For a full description of opcodes, configuration save paths, reload order, and
|
||||||
|
logging wire ids, see @subpage proto_lipc_framed.
|
||||||
|
|
||||||
|
@section proto_deprecated Legacy NUL-separated sockets (deprecated)
|
||||||
|
|
||||||
|
The **legacy command** socket (\c x52d.cmd) and **legacy notify** socket
|
||||||
|
(\c x52d.notify) use a NUL-terminated “argv” style wire format. They remain
|
||||||
|
available **only during migration** of existing tools and scripts.
|
||||||
|
|
||||||
|
\b These legacy sockets and their wire format are \b deprecated and \b will be
|
||||||
|
\b removed in a future release. New code must use the framed LIPC socket
|
||||||
|
described above.
|
||||||
|
|
||||||
|
Default paths are under `$(LOCALSTATEDIR)/run/` (see \c X52D_SOCK_COMMAND and
|
||||||
|
\c X52D_SOCK_NOTIFY in the daemon). The daemon overrides them with \c -s
|
||||||
|
(command) and \c -b (notify).
|
||||||
|
|
||||||
|
The legacy client helpers in \ref x52dcomm.h (\c x52d_dial_command,
|
||||||
|
\c x52d_format_command, \c x52d_send_command, \c x52d_dial_notify,
|
||||||
|
\c x52d_recv_notification) are deprecated with the **same removal policy** as
|
||||||
|
the sockets. Use \c x52d_ipc_socket_path, \c x52d_dial_ipc, and \c x52d_ipc_call
|
||||||
|
instead.
|
||||||
|
|
||||||
|
@subsection proto_legacy_cmd Legacy NUL command protocol
|
||||||
|
|
||||||
|
\b x52d requires that clients send commands as a series of NUL terminated
|
||||||
strings, without any interleaving space. The command should be sent in a
|
strings, without any interleaving space. The command should be sent in a
|
||||||
single `send` call, and the client may expect a response in a single `recv`
|
single \c send call, and the client may expect a response in a single \c recv
|
||||||
call.
|
call.
|
||||||
|
|
||||||
The `send` call must send exactly the number of bytes in the command text.
|
The \c send call must send exactly the number of bytes in the command text.
|
||||||
Extra bytes will be treated as additional arguments, which would cause the
|
Extra bytes will be treated as additional arguments, which would cause the
|
||||||
command to fail. It is recommended that the `recv` call uses a 1024 byte buffer
|
command to fail. It is recommended that the \c recv call uses a 1024 byte buffer
|
||||||
to read the data. Responses will never exceed this length.
|
to read the data. Responses will never exceed this length.
|
||||||
|
|
||||||
# Responses
|
@subsubsection proto_legacy_resp Responses
|
||||||
|
|
||||||
The daemon sends the response as a series of NUL terminated strings, without
|
The daemon sends the response as a series of NUL terminated strings, without
|
||||||
any interleaving space. The first string is always one of the following:
|
any interleaving space. The first string is always one of the following:
|
||||||
|
|
@ -30,40 +62,52 @@ any interleaving space. The first string is always one of the following:
|
||||||
This determines whether the request was successful or not, and subsequent
|
This determines whether the request was successful or not, and subsequent
|
||||||
strings describe the action, error or requested data.
|
strings describe the action, error or requested data.
|
||||||
|
|
||||||
# Examples
|
@subsubsection proto_legacy_ex Examples
|
||||||
|
|
||||||
## Reloading configuration
|
|
||||||
|
|
||||||
|
@par Reloading configuration
|
||||||
- \b send <tt>config\0reload\0</tt>
|
- \b send <tt>config\0reload\0</tt>
|
||||||
- \b recv <tt>OK\0config\0reload\0</tt>
|
- \b recv <tt>OK\0config\0reload\0</tt>
|
||||||
|
|
||||||
## Reading mouse speed
|
@par Reading mouse speed
|
||||||
|
|
||||||
- \b send <tt>config\0get\0mouse\0speed\0</tt>
|
- \b send <tt>config\0get\0mouse\0speed\0</tt>
|
||||||
- \b recv <tt>DATA\0mouse\0speed\010\0</tt>
|
- \b recv <tt>DATA\0mouse\0speed\010\0</tt>
|
||||||
|
|
||||||
## Sending an invalid command
|
@par Sending an invalid command
|
||||||
|
|
||||||
- \b send <tt>config reload</tt>
|
- \b send <tt>config reload</tt>
|
||||||
- \b recv <tt>ERR\0Unknown command 'config reload'\0</tt>
|
- \b recv <tt>ERR\0Unknown command 'config reload'\0</tt>
|
||||||
|
|
||||||
# Commands
|
@subsection proto_legacy_vocab Legacy command vocabulary
|
||||||
|
|
||||||
\b x52d commands are arranged in a hierarchical fashion as follows:
|
\b x52d commands are arranged in a hierarchical fashion as follows:
|
||||||
|
|
||||||
```
|
@code{.unparsed}
|
||||||
<command-group> [<sub-command-group> [<sub-command-group> [...]]] <command> [<arguments>]
|
<command-group> [<sub-command-group> [<sub-command-group> [...]]] <command> [<arguments>]
|
||||||
```
|
@endcode
|
||||||
|
|
||||||
The list of supported commands are shown below:
|
The list of supported **legacy NUL** commands are shown below (superseded on
|
||||||
|
the wire by @ref proto_lipc_framed where an opcode exists):
|
||||||
|
|
||||||
- @subpage proto_config
|
- @subpage proto_config
|
||||||
- @subpage proto_logging
|
- @subpage proto_logging
|
||||||
|
|
||||||
|
@subsection proto_legacy_notify Legacy notify socket (deprecated)
|
||||||
|
|
||||||
|
Subscribers connect to the legacy notify socket (\c x52d.notify by default).
|
||||||
|
The daemon broadcasts short NUL-framed messages (same packing style as the
|
||||||
|
legacy command responses) when internal events occur. Notably, device connect
|
||||||
|
and disconnect were historically reported as string events such as
|
||||||
|
\c CONNECTED and \c DISCONNECTED.
|
||||||
|
|
||||||
|
On the **framed LIPC** socket, the same information is carried by the
|
||||||
|
\c DEVICE_STATE push; see @ref lipc_push_device_state. Prefer subscribing on
|
||||||
|
the unified LIPC path before legacy removal.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@page proto_config Configuration management
|
@page proto_config Configuration management (legacy NUL command socket)
|
||||||
|
|
||||||
|
@note This page documents the **deprecated** NUL-separated \c x52d.cmd protocol.
|
||||||
|
For new integrations use @ref proto_lipc_framed (\c CONFIG_* opcodes).
|
||||||
|
|
||||||
The \c config commands deal with \b x52d configuration subsystem, and have the
|
The \c config commands deal with \b x52d configuration subsystem, and have the
|
||||||
following subcommands.
|
following subcommands.
|
||||||
|
|
@ -223,7 +267,10 @@ ERR\0Error 22 setting 'led.fire'='none': Invalid argument\0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@page proto_logging Logging management
|
@page proto_logging Logging management (legacy NUL command socket)
|
||||||
|
|
||||||
|
@note This page documents the **deprecated** NUL-separated \c x52d.cmd protocol.
|
||||||
|
For new integrations use @ref proto_lipc_framed (\c LOGGING_SHOW / \c LOGGING_SET).
|
||||||
|
|
||||||
The \c logging commands allow the user to fine tune the logging configuration
|
The \c logging commands allow the user to fine tune the logging configuration
|
||||||
of \c x52d as well as adjust the log levels for either all the modules, or for
|
of \c x52d as well as adjust the log levels for either all the modules, or for
|
||||||
|
|
@ -306,3 +353,110 @@ otherwise.
|
||||||
- <tt>\a module-name</tt> (if specified)
|
- <tt>\a module-name</tt> (if specified)
|
||||||
- \a log-level
|
- \a log-level
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@page proto_lipc_framed Framed LIPC control and notify (x52d.socket)
|
||||||
|
|
||||||
|
The daemon exposes **liblocalipc** on a UNIX stream socket (default under
|
||||||
|
`$(LOCALSTATEDIR)/run/`, basename \c x52d.socket, overridable with \c -S). This
|
||||||
|
is the **only** supported long-term path for control-plane RPC and for **push**
|
||||||
|
notifications. Request opcodes for configuration and logging match
|
||||||
|
\ref x52d_ipc.h; field semantics and on-disk layout are summarized here.
|
||||||
|
|
||||||
|
@section lipc_push_device_state LIPC push: DEVICE_STATE (\c tid == 0)
|
||||||
|
|
||||||
|
When the USB device connects or disconnects, the daemon may emit a **push**
|
||||||
|
frame: \c lipc_header.tid is **zero**, and \c lipc_header.request identifies the
|
||||||
|
push type \c DEVICE_STATE, wire id \c X52D_IPC_PUSH_DEVICE_STATE (\c 0x8001) in
|
||||||
|
\ref x52d_ipc.h — distinct from RPC opcodes \c 0x01–\c 0x08 and \c 0x11–\c 0x12.
|
||||||
|
|
||||||
|
Semantics:
|
||||||
|
|
||||||
|
- \c lipc_header.index — **connection state**: \c 0 = disconnected, \c 1 = connected.
|
||||||
|
- \c lipc_header.value — USB identity in the **lower 32 bits**:
|
||||||
|
\c (uint32_t)((idVendor << 16) | idProduct) with 16-bit USB \c idVendor and
|
||||||
|
\c idProduct (upper bits reserved, zero unless extended later). On **connect**
|
||||||
|
(\c index == 1), this reflects the **current** device. On **disconnect**
|
||||||
|
(\c index == 0), the daemon **preserves the last connected** vendor/product id
|
||||||
|
so clients know **which** device dropped; if no device was ever connected in
|
||||||
|
this process lifetime, the lower 32 bits are \c 0.
|
||||||
|
- **Payload** — optional UTF-8. When **connected** (\c index == 1), the daemon
|
||||||
|
**may** include the USB **product** string (\c lipc_header.length > 0). Clients
|
||||||
|
must accept **zero-length** payload. When **disconnected**, payload is
|
||||||
|
normally empty.
|
||||||
|
- \c lipc_header.status — typically \c LIPC_OK for a well-formed push.
|
||||||
|
|
||||||
|
This push supersedes the legacy notify strings \c CONNECTED / \c DISCONNECTED
|
||||||
|
on the deprecated \c x52d.notify socket.
|
||||||
|
|
||||||
|
@section lipc_state_path Runtime configuration file
|
||||||
|
|
||||||
|
Persistent settings written by @c CONFIG_SAVE (and removed by @c CONFIG_CLEAR
|
||||||
|
when targeting state) live at:
|
||||||
|
|
||||||
|
@code
|
||||||
|
$(LOCALSTATEDIR)/lib/x52d/x52d.conf
|
||||||
|
@endcode
|
||||||
|
|
||||||
|
(\c X52D_STATE_CFG_FILE in daemon sources). The daemon writes this path with a temporary file in
|
||||||
|
the same directory followed by @c rename(2) so readers never see a torn file.
|
||||||
|
Static defaults shipped by the distribution use \c X52D_SYS_CFG_FILE under
|
||||||
|
@c $(SYSCONFDIR).
|
||||||
|
|
||||||
|
@section lipc_config_get_set CONFIG_GET and CONFIG_SET (registry ids)
|
||||||
|
|
||||||
|
@c CONFIG_SET (@c 0x07) and @c CONFIG_GET (@c 0x08) address a single configuration key using
|
||||||
|
numeric identifiers from the build-time registry (@c config_registry.json → @c config-defs.h /
|
||||||
|
@c config-defs.c), not free-form section/option strings on the wire:
|
||||||
|
|
||||||
|
- @c lipc_header.index — section id (e.g. @c CFG_SECTION_CLOCK, @c CFG_SECTION_LED, ...).
|
||||||
|
- @c lipc_header.value — option id within that section (e.g. @c CFG_OPTION_CLOCK_ENABLED).
|
||||||
|
The value must fit in 16 bits; garbage in the upper bits of the 64-bit wire field is rejected.
|
||||||
|
|
||||||
|
@c CONFIG_SET request payload is the new value as a UTF-8 string (length = @c lipc_header.length).
|
||||||
|
The daemon validates @c (index, value) with @c x52d_config_registry_pair_valid(), resolves names
|
||||||
|
via @c section_names / @c option_names, then applies the same parsing as file-based configuration
|
||||||
|
(@c x52d_config_set + @c x52d_config_apply_immediate).
|
||||||
|
|
||||||
|
@c CONFIG_GET uses the same @c index / @c value selection; the reply payload is a single string,
|
||||||
|
the same logical text as other dump paths (@c x52d_config_get / dumpers), not the legacy
|
||||||
|
NUL-separated @c DATA argv shape.
|
||||||
|
|
||||||
|
@section lipc_config_clear CONFIG_CLEAR targets
|
||||||
|
|
||||||
|
The LIPC @c CONFIG_CLEAR request selects a file via @c lipc_header.index:
|
||||||
|
|
||||||
|
- @c X52D_IPC_CONFIG_CLEAR_TARGET_STATE (@c 1): delete the runtime state file
|
||||||
|
(\ref lipc_state_path).
|
||||||
|
- @c X52D_IPC_CONFIG_CLEAR_TARGET_SYSCONF (@c 2): delete the system configuration file
|
||||||
|
(\c X52D_SYS_CFG_FILE under \c $(SYSCONFDIR)).
|
||||||
|
|
||||||
|
After attempting @c unlink(2), the daemon always runs the same reload sequence
|
||||||
|
as @c CONFIG_RELOAD: load the state file if it exists, else the sysconf file if
|
||||||
|
it exists, else defaults (then apply CLI overrides). If deleting the sysconf file
|
||||||
|
fails (e.g. read-only filesystem), the reply uses a non-success status with the
|
||||||
|
unlink error in the payload, but reload and apply still run so runtime state
|
||||||
|
stays consistent.
|
||||||
|
|
||||||
|
@section lipc_config_reload_order Canonical reload order
|
||||||
|
|
||||||
|
@c CONFIG_RELOAD and the post-@c CONFIG_CLEAR reload both prefer the state file
|
||||||
|
when present and readable, then the system configuration file, otherwise
|
||||||
|
in-memory defaults.
|
||||||
|
|
||||||
|
@section lipc_logging_show_set LOGGING_SHOW (@c 0x11) and LOGGING_SET (@c 0x12)
|
||||||
|
|
||||||
|
Logging control uses numeric ids from the same build-time tables as the legacy
|
||||||
|
NUL command path (@c module_defs.py → @c module-map.h / @c module-map.c and
|
||||||
|
@c loglevel_map / @c lookup_level_by_id). There are no module or level names on the LIPC wire.
|
||||||
|
|
||||||
|
- @c lipc_header.index — module id (@c X52D_MOD_* values, @c 0 … @c X52D_MOD_MAX - 1), or
|
||||||
|
@c X52D_MOD_GLOBAL (@c 0xFF) for the global log level. Clients must use @c x52d_module_wire_valid()
|
||||||
|
semantics (or the equivalent): any other @c index is invalid.
|
||||||
|
- @c lipc_header.value — for @c LOGGING_SET only, the log level id in the same numeric space as
|
||||||
|
@c lookup_level_by_id (including @c FATAL as @c 0 and negative ids such as @c NOTSET where present
|
||||||
|
in the map, encoded in the 64-bit field as two's-complement). Validated with @c x52d_log_level_wire_valid().
|
||||||
|
For @c LOGGING_SHOW, @c value must be zero (@c LIPC_FIELD_FORBIDDEN); the current level is read from pinelog.
|
||||||
|
|
||||||
|
@c LOGGING_SHOW reply payload is a single string (the level label, e.g. @c info), not the legacy @c DATA argv shape.
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class TestCase:
|
||||||
print(out)
|
print(out)
|
||||||
|
|
||||||
cmd = [suite.find_control_program(),
|
cmd = [suite.find_control_program(),
|
||||||
'-s', suite.command, '--',
|
'-s', suite.ipc_socket,
|
||||||
*self.in_cmd]
|
*self.in_cmd]
|
||||||
|
|
||||||
testcase = subprocess.run(cmd, stdout=subprocess.PIPE, check=False)
|
testcase = subprocess.run(cmd, stdout=subprocess.PIPE, check=False)
|
||||||
|
|
@ -75,6 +75,7 @@ class Test:
|
||||||
self.tmpdir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
|
self.tmpdir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
|
||||||
self.command = os.path.join(self.tmpdir.name, "x52d.cmd")
|
self.command = os.path.join(self.tmpdir.name, "x52d.cmd")
|
||||||
self.notify = os.path.join(self.tmpdir.name, "x52d.notify")
|
self.notify = os.path.join(self.tmpdir.name, "x52d.notify")
|
||||||
|
self.ipc_socket = os.path.join(self.tmpdir.name, "x52d.socket")
|
||||||
self.daemon = None
|
self.daemon = None
|
||||||
self.testcases = []
|
self.testcases = []
|
||||||
|
|
||||||
|
|
@ -128,6 +129,7 @@ class Test:
|
||||||
"-p", os.path.join(self.tmpdir.name, "x52d.pid"), # PID file
|
"-p", os.path.join(self.tmpdir.name, "x52d.pid"), # PID file
|
||||||
"-s", self.command, # Command socket path
|
"-s", self.command, # Command socket path
|
||||||
"-b", self.notify, # Notification socket path
|
"-b", self.notify, # Notification socket path
|
||||||
|
"-S", self.ipc_socket, # Unified framed IPC socket path
|
||||||
]
|
]
|
||||||
|
|
||||||
# Create empty config file
|
# Create empty config file
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||||
|
exec "@PYTHON@" "@X52CTL_PYMAIN@" "$@"
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
@page x52ctl Command line controller to X52 daemon (Python)
|
||||||
|
|
||||||
|
\htmlonly
|
||||||
|
<b>x52ctl</b> - Command line controller to X52 daemon
|
||||||
|
\endhtmlonly
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
<tt>\b x52ctl [\a -i] [\a -s PATH] \b config \a ...</tt>
|
||||||
|
<br/>
|
||||||
|
<tt>\b x52ctl [\a -i] [\a -s PATH] \b logging \a ...</tt>
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
x52ctl sends framed LIPC requests to the running x52d daemon on its unified
|
||||||
|
control socket (see @ref x52d_ipc.h and @ref proto_lipc_framed). It can be used
|
||||||
|
as a one-shot program from scripts or interactively.
|
||||||
|
|
||||||
|
Responses are written to standard output in the same NUL-separated form as the
|
||||||
|
legacy command socket used historically (\c OK / \c ERR / \c DATA followed by
|
||||||
|
NUL-terminated string fields), so existing parsers can keep working.
|
||||||
|
|
||||||
|
If not running interactively, a full command (e.g. \c config reload) is required
|
||||||
|
or the program exits with a non-zero status. In interactive mode (\c -i),
|
||||||
|
lines are read from standard input and executed until \c quit or end-of-file.
|
||||||
|
|
||||||
|
# OPTIONS
|
||||||
|
|
||||||
|
- <tt>\b -i</tt>, <tt>\b --interactive</tt>
|
||||||
|
Run in interactive mode. Extra non-option arguments are ignored.
|
||||||
|
|
||||||
|
- <tt>\b -s \a PATH</tt>, <tt>\b --socket \a PATH</tt>
|
||||||
|
Use the daemon framed IPC socket at \a PATH. If omitted, the default path
|
||||||
|
compiled into libx52dcomm is used (same layout as the x52d \c RUNDIR socket).
|
||||||
|
|
||||||
|
# COMMANDS
|
||||||
|
|
||||||
|
Subcommands are grouped under \b config and \b logging (see \c x52ctl --help).
|
||||||
|
|
||||||
|
# SEE ALSO
|
||||||
|
@ref x52d, @ref x52dcomm, @ref proto_lipc_framed
|
||||||
|
*/
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!@PYTHON@
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||||
|
"""Uninstalled launcher: prepend build dir for generated config_defs."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
_BUILD_PKG = r'@X52D_PY_PKG@'
|
||||||
|
if _BUILD_PKG and _BUILD_PKG not in sys.path:
|
||||||
|
sys.path.insert(0, _BUILD_PKG)
|
||||||
|
|
||||||
|
_here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if 'X52DCOMM_LIB' not in os.environ:
|
||||||
|
for _name in (
|
||||||
|
'libx52dcomm.so',
|
||||||
|
'libx52dcomm.so.1',
|
||||||
|
'libx52dcomm.so.1.0.0',
|
||||||
|
'libx52dcomm.dylib',
|
||||||
|
):
|
||||||
|
_cand = os.path.join(_here, _name)
|
||||||
|
if os.path.isfile(_cand):
|
||||||
|
os.environ['X52DCOMM_LIB'] = _cand
|
||||||
|
break
|
||||||
|
|
||||||
|
_src_dir = os.path.normpath(os.path.join(_here, '..', '..', 'daemon'))
|
||||||
|
if os.path.isfile(os.path.join(_src_dir, 'x52ctl_main.py')) and _src_dir not in sys.path:
|
||||||
|
sys.path.insert(0, _src_dir)
|
||||||
|
|
||||||
|
from x52ctl_main import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,615 @@
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||||
|
"""x52ctl: CLI for x52d using framed LIPC (libx52dcomm)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
|
||||||
|
_pkg = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if _pkg not in sys.path:
|
||||||
|
sys.path.insert(0, _pkg)
|
||||||
|
from ctypes import (
|
||||||
|
CDLL,
|
||||||
|
POINTER,
|
||||||
|
Structure,
|
||||||
|
byref,
|
||||||
|
c_char_p,
|
||||||
|
c_int,
|
||||||
|
c_size_t,
|
||||||
|
c_uint16,
|
||||||
|
c_uint32,
|
||||||
|
c_uint64,
|
||||||
|
c_void_p,
|
||||||
|
cast,
|
||||||
|
create_string_buffer,
|
||||||
|
get_errno,
|
||||||
|
)
|
||||||
|
|
||||||
|
import module_defs
|
||||||
|
from command_defs import ConfigClearTarget, IpcRequest, X52D_MOD_GLOBAL
|
||||||
|
|
||||||
|
LIPC_OK = 0
|
||||||
|
|
||||||
|
|
||||||
|
class LIPCHeader(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('magic', c_uint32),
|
||||||
|
('version', c_uint16),
|
||||||
|
('request', c_uint16),
|
||||||
|
('status', c_uint16),
|
||||||
|
('index', c_uint16),
|
||||||
|
('tid', c_uint32),
|
||||||
|
('length', c_uint32),
|
||||||
|
('checksum', c_uint32),
|
||||||
|
('value', c_uint64),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _load_comm_lib() -> CDLL:
|
||||||
|
path = os.environ.get('X52DCOMM_LIB')
|
||||||
|
names = (path, 'libx52dcomm.so.1', 'libx52dcomm.so')
|
||||||
|
last: OSError | None = None
|
||||||
|
for cand in names:
|
||||||
|
if not cand:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
return CDLL(cand)
|
||||||
|
except OSError as exc: # pragma: no cover
|
||||||
|
last = exc
|
||||||
|
raise OSError(last or 'cannot load libx52dcomm')
|
||||||
|
|
||||||
|
|
||||||
|
_LIB = None
|
||||||
|
|
||||||
|
|
||||||
|
def _lib() -> CDLL:
|
||||||
|
global _LIB
|
||||||
|
if _LIB is None:
|
||||||
|
_LIB = _load_comm_lib()
|
||||||
|
_LIB.x52d_dial_ipc.argtypes = [c_char_p]
|
||||||
|
_LIB.x52d_dial_ipc.restype = c_int
|
||||||
|
_LIB.x52d_ipc_call.argtypes = [
|
||||||
|
c_int,
|
||||||
|
c_uint16,
|
||||||
|
c_uint16,
|
||||||
|
c_uint64,
|
||||||
|
c_void_p,
|
||||||
|
c_size_t,
|
||||||
|
POINTER(LIPCHeader),
|
||||||
|
c_void_p,
|
||||||
|
c_size_t,
|
||||||
|
POINTER(c_size_t),
|
||||||
|
]
|
||||||
|
_LIB.x52d_ipc_call.restype = c_int
|
||||||
|
return _LIB
|
||||||
|
|
||||||
|
|
||||||
|
def _nul_out(parts: list[str]) -> bytes:
|
||||||
|
return b'\0'.join(p.encode('utf-8') for p in parts) + b'\0'
|
||||||
|
|
||||||
|
|
||||||
|
def _ipc_call(
|
||||||
|
fd: int,
|
||||||
|
request: int,
|
||||||
|
index: int,
|
||||||
|
value: int,
|
||||||
|
payload: bytes | None,
|
||||||
|
reply_cap: int = 512 * 1024,
|
||||||
|
) -> tuple[int, LIPCHeader, bytes]:
|
||||||
|
pl = payload or b''
|
||||||
|
hdr = LIPCHeader()
|
||||||
|
rbuf = create_string_buffer(reply_cap)
|
||||||
|
rlen = c_size_t(0)
|
||||||
|
if pl:
|
||||||
|
pl_buf = create_string_buffer(pl, len(pl))
|
||||||
|
pl_ptr = cast(pl_buf, c_void_p)
|
||||||
|
pl_len = len(pl)
|
||||||
|
else:
|
||||||
|
pl_ptr = None
|
||||||
|
pl_len = 0
|
||||||
|
st = _lib().x52d_ipc_call(
|
||||||
|
fd,
|
||||||
|
request,
|
||||||
|
index,
|
||||||
|
value,
|
||||||
|
pl_ptr,
|
||||||
|
pl_len,
|
||||||
|
byref(hdr),
|
||||||
|
rbuf,
|
||||||
|
reply_cap,
|
||||||
|
byref(rlen),
|
||||||
|
)
|
||||||
|
raw = bytes(rbuf[: rlen.value])
|
||||||
|
if int(st) != LIPC_OK:
|
||||||
|
return int(st), hdr, raw
|
||||||
|
# lipc_client_call returns LIPC_OK when a reply frame arrived; check wire status.
|
||||||
|
if int(hdr.status) != LIPC_OK:
|
||||||
|
return int(hdr.status), hdr, raw
|
||||||
|
return LIPC_OK, hdr, raw
|
||||||
|
|
||||||
|
|
||||||
|
def _errno_from_strerror(msg: str) -> int | None:
|
||||||
|
for code in range(1, 200):
|
||||||
|
try:
|
||||||
|
if os.strerror(code) == msg:
|
||||||
|
return code
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _format_config_set_err(sec: str, opt: str, val: str, srv: str) -> str:
|
||||||
|
n = _errno_from_strerror(srv)
|
||||||
|
if n is not None:
|
||||||
|
return f"Error {n} setting '{sec}.{opt}'='{val}': {srv}"
|
||||||
|
return srv
|
||||||
|
|
||||||
|
|
||||||
|
def _map_config_get_err(sec: str, opt: str, srv: str) -> str:
|
||||||
|
if 'configuration key not found' in srv or srv == 'configuration key not found':
|
||||||
|
return f"Error getting '{sec}.{opt}'"
|
||||||
|
return srv
|
||||||
|
|
||||||
|
|
||||||
|
def _import_config_defs():
|
||||||
|
import config_defs as cd # pylint: disable=import-outside-toplevel
|
||||||
|
|
||||||
|
return cd
|
||||||
|
|
||||||
|
|
||||||
|
def _config_wire(sec: str, opt: str):
|
||||||
|
cd = _import_config_defs()
|
||||||
|
sec_id = getattr(cd.Section, sec.upper()).value
|
||||||
|
opt_cls = getattr(cd, cd.Section(sec_id).name)
|
||||||
|
opt_id = getattr(opt_cls, opt.upper()).value
|
||||||
|
return sec_id, opt_id
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_module(name: str) -> int | None:
|
||||||
|
try:
|
||||||
|
return module_defs.Module[name.upper()].value
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_level(name: str) -> int | None:
|
||||||
|
key = name.lower()
|
||||||
|
for lvl in module_defs.LogLevel:
|
||||||
|
lbl = 'default' if lvl == module_defs.LogLevel.NOTSET else lvl.name.lower()
|
||||||
|
if lbl == key:
|
||||||
|
return int(lvl.value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _level_to_wire(level: int) -> int:
|
||||||
|
return level & ((1 << 64) - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _open_ipc(sock_path: str | None) -> int:
|
||||||
|
arg = sock_path.encode('utf-8') if sock_path else None
|
||||||
|
fd = _lib().x52d_dial_ipc(arg)
|
||||||
|
if fd < 0:
|
||||||
|
err = get_errno()
|
||||||
|
raise OSError(err, os.strerror(err))
|
||||||
|
return fd
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_load(fd: int, path: str) -> bytes:
|
||||||
|
if not path:
|
||||||
|
return _nul_out(['ERR', "Invalid file '' for 'config load' command"])
|
||||||
|
try:
|
||||||
|
if not os.path.exists(path) or not os.access(path, os.R_OK):
|
||||||
|
return _nul_out(['ERR', f"Invalid file '{path}' for 'config load' command"])
|
||||||
|
except OSError:
|
||||||
|
return _nul_out(['ERR', f"Invalid file '{path}' for 'config load' command"])
|
||||||
|
st, _hdr, pl = _ipc_call(fd, IpcRequest.CONFIG_LOAD, 0, 0, os.fsencode(path))
|
||||||
|
if st != LIPC_OK:
|
||||||
|
return _nul_out(['ERR', os.fsdecode(pl) if pl else f'IPC error {st}'])
|
||||||
|
return _nul_out(['OK', 'config', 'load', path])
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_reload(fd: int) -> bytes:
|
||||||
|
st, _h, pl = _ipc_call(fd, IpcRequest.CONFIG_RELOAD, 0, 0, None)
|
||||||
|
if st != LIPC_OK:
|
||||||
|
return _nul_out(['ERR', pl.decode('utf-8', 'replace') if pl else f'IPC error {st}'])
|
||||||
|
return _nul_out(['OK', 'config', 'reload'])
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_reset(fd: int) -> bytes:
|
||||||
|
st, _h, pl = _ipc_call(fd, IpcRequest.CONFIG_RESET, 0, 0, None)
|
||||||
|
if st != LIPC_OK:
|
||||||
|
return _nul_out(['ERR', pl.decode('utf-8', 'replace') if pl else f'IPC error {st}'])
|
||||||
|
return _nul_out(['OK', 'config', 'reset'])
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_clear(fd: int, target: str) -> bytes:
|
||||||
|
tmap = {'state': ConfigClearTarget.STATE, 'sysconf': ConfigClearTarget.SYSCONF}
|
||||||
|
if target not in tmap:
|
||||||
|
return _nul_out(['ERR', f'unknown clear target {target!r}'])
|
||||||
|
st, _h, pl = _ipc_call(fd, IpcRequest.CONFIG_CLEAR, int(tmap[target]), 0, None)
|
||||||
|
if st != LIPC_OK:
|
||||||
|
return _nul_out(['ERR', pl.decode('utf-8', 'replace') if pl else f'IPC error {st}'])
|
||||||
|
return _nul_out(['OK', 'config', 'clear', target])
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_save(fd: int) -> bytes:
|
||||||
|
st, _h, pl = _ipc_call(fd, IpcRequest.CONFIG_SAVE, 0, 0, None)
|
||||||
|
if st != LIPC_OK:
|
||||||
|
return _nul_out(['ERR', pl.decode('utf-8', 'replace') if pl else f'IPC error {st}'])
|
||||||
|
return _nul_out(['OK', 'config', 'save'])
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_dump(fd: int, out_path: str) -> bytes:
|
||||||
|
if not out_path:
|
||||||
|
return _nul_out(['ERR', "Invalid file '' for 'config dump' command"])
|
||||||
|
parent = os.path.dirname(os.path.abspath(out_path)) or '/'
|
||||||
|
if not os.path.isdir(parent):
|
||||||
|
return _nul_out(['ERR', f"Invalid file '{out_path}' for 'config dump' command"])
|
||||||
|
st, _h, pl = _ipc_call(fd, IpcRequest.CONFIG_DUMP, 0, 0, None)
|
||||||
|
if st != LIPC_OK:
|
||||||
|
return _nul_out(['ERR', pl.decode('utf-8', 'replace') if pl else f'IPC error {st}'])
|
||||||
|
try:
|
||||||
|
with open(out_path, 'wb') as out_f:
|
||||||
|
out_f.write(pl)
|
||||||
|
except OSError as exc:
|
||||||
|
return _nul_out(['ERR', str(exc)])
|
||||||
|
return _nul_out(['OK', 'config', 'dump', out_path])
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_get(fd: int, sec: str, opt: str) -> bytes:
|
||||||
|
try:
|
||||||
|
sid, oid = _config_wire(sec, opt)
|
||||||
|
except (AttributeError, KeyError, ValueError):
|
||||||
|
return _nul_out(['ERR', _map_config_get_err(sec, opt, 'configuration key not found')])
|
||||||
|
st, _h, pl = _ipc_call(fd, IpcRequest.CONFIG_GET, sid, oid, None)
|
||||||
|
if st != LIPC_OK:
|
||||||
|
msg = pl.decode('utf-8', 'replace') if pl else ''
|
||||||
|
return _nul_out(['ERR', _map_config_get_err(sec, opt, msg)])
|
||||||
|
val = pl.decode('utf-8', 'replace')
|
||||||
|
return _nul_out(['DATA', sec, opt, val])
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_set(fd: int, sec: str, opt: str, val: str) -> bytes:
|
||||||
|
try:
|
||||||
|
sid, oid = _config_wire(sec, opt)
|
||||||
|
except (AttributeError, KeyError, ValueError):
|
||||||
|
n = errno.EINVAL
|
||||||
|
return _nul_out(['ERR', _format_config_set_err(sec, opt, val, os.strerror(n))])
|
||||||
|
st, _h, pl = _ipc_call(fd, IpcRequest.CONFIG_SET, sid, oid, val.encode('utf-8'))
|
||||||
|
if st != LIPC_OK:
|
||||||
|
msg = pl.decode('utf-8', 'replace') if pl else ''
|
||||||
|
return _nul_out(['ERR', _format_config_set_err(sec, opt, val, msg)])
|
||||||
|
return _nul_out(['OK', 'config', 'set', sec, opt, val])
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_logging_show(fd: int, module: str | None) -> bytes:
|
||||||
|
if module is None:
|
||||||
|
idx = X52D_MOD_GLOBAL
|
||||||
|
label = 'global'
|
||||||
|
else:
|
||||||
|
mid = _lookup_module(module)
|
||||||
|
if mid is None:
|
||||||
|
return _nul_out(['ERR', f"Invalid module '{module}'"])
|
||||||
|
idx = mid
|
||||||
|
label = module.lower()
|
||||||
|
st, _h, pl = _ipc_call(fd, IpcRequest.LOGGING_SHOW, idx, 0, None)
|
||||||
|
if st != LIPC_OK:
|
||||||
|
return _nul_out(['ERR', pl.decode('utf-8', 'replace') if pl else f'IPC error {st}'])
|
||||||
|
lvl_name = pl.decode('utf-8', 'replace').strip()
|
||||||
|
return _nul_out(['DATA', label, lvl_name])
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_logging_set(fd: int, module: str | None, level_name: str) -> bytes:
|
||||||
|
level = _lookup_level(level_name)
|
||||||
|
if level is None:
|
||||||
|
return _nul_out(['ERR', f"Unknown level '{level_name}' for 'logging set' command"])
|
||||||
|
if module is None:
|
||||||
|
if level == int(module_defs.LogLevel.NOTSET.value):
|
||||||
|
return _nul_out(["ERR", "'default' level is not valid without a module"])
|
||||||
|
st, _h, pl = _ipc_call(
|
||||||
|
fd, IpcRequest.LOGGING_SET, X52D_MOD_GLOBAL, _level_to_wire(level), None
|
||||||
|
)
|
||||||
|
if st != LIPC_OK:
|
||||||
|
return _nul_out(['ERR', pl.decode('utf-8', 'replace') if pl else f'IPC error {st}'])
|
||||||
|
return _nul_out(['OK', 'logging', 'set', level_name.lower()])
|
||||||
|
mid = _lookup_module(module)
|
||||||
|
if mid is None:
|
||||||
|
return _nul_out(['ERR', f"Invalid module '{module}'"])
|
||||||
|
st, _h, pl = _ipc_call(fd, IpcRequest.LOGGING_SET, mid, _level_to_wire(level), None)
|
||||||
|
if st != LIPC_OK:
|
||||||
|
return _nul_out(['ERR', pl.decode('utf-8', 'replace') if pl else f'IPC error {st}'])
|
||||||
|
return _nul_out(['OK', 'logging', 'set', module.lower(), level_name.lower()])
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_argc_errors(argv: list[str]) -> bytes | None:
|
||||||
|
if not argv:
|
||||||
|
return None
|
||||||
|
if argv[0] == 'config':
|
||||||
|
if len(argv) < 2:
|
||||||
|
return _nul_out(['ERR', "Insufficient arguments for 'config' command"])
|
||||||
|
sub = argv[1]
|
||||||
|
if sub == 'load':
|
||||||
|
if len(argv) != 3:
|
||||||
|
return _nul_out(
|
||||||
|
[
|
||||||
|
'ERR',
|
||||||
|
f"Unexpected arguments for 'config load' command; got {len(argv)}, expected 3",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif sub == 'reload':
|
||||||
|
if len(argv) != 2:
|
||||||
|
return _nul_out(
|
||||||
|
[
|
||||||
|
'ERR',
|
||||||
|
f"Unexpected arguments for 'config reload' command; got {len(argv)}, expected 2",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif sub == 'dump':
|
||||||
|
if len(argv) != 3:
|
||||||
|
return _nul_out(
|
||||||
|
[
|
||||||
|
'ERR',
|
||||||
|
f"Unexpected arguments for 'config dump' command; got {len(argv)}, expected 3",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif sub == 'save':
|
||||||
|
if len(argv) != 2:
|
||||||
|
return _nul_out(
|
||||||
|
[
|
||||||
|
'ERR',
|
||||||
|
f"Unexpected arguments for 'config save' command; got {len(argv)}, expected 2",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif sub == 'clear':
|
||||||
|
if len(argv) != 3:
|
||||||
|
return _nul_out(
|
||||||
|
[
|
||||||
|
'ERR',
|
||||||
|
f"Unexpected arguments for 'config clear' command; got {len(argv)}, expected 3",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif sub == 'get':
|
||||||
|
if len(argv) != 4:
|
||||||
|
return _nul_out(
|
||||||
|
[
|
||||||
|
'ERR',
|
||||||
|
f"Unexpected arguments for 'config get' command; got {len(argv)}, expected 4",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif sub == 'set':
|
||||||
|
if len(argv) != 5:
|
||||||
|
return _nul_out(
|
||||||
|
[
|
||||||
|
'ERR',
|
||||||
|
f"Unexpected arguments for 'config set' command; got {len(argv)}, expected 5",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif sub == '':
|
||||||
|
return _nul_out(['ERR', "Unknown subcommand '' for 'config' command"])
|
||||||
|
elif sub not in ('load', 'reload', 'reset', 'save', 'dump', 'clear', 'get', 'set'):
|
||||||
|
return _nul_out(['ERR', f"Unknown subcommand '{sub}' for 'config' command"])
|
||||||
|
if argv[0] == 'logging':
|
||||||
|
if len(argv) < 2:
|
||||||
|
return _nul_out(['ERR', "Insufficient arguments for 'logging' command"])
|
||||||
|
sub = argv[1]
|
||||||
|
if sub not in ('show', 'set'):
|
||||||
|
return _nul_out(['ERR', f"Unknown subcommand '{sub}' for 'logging' command"])
|
||||||
|
if sub == 'show' and len(argv) > 3:
|
||||||
|
return _nul_out(
|
||||||
|
[
|
||||||
|
'ERR',
|
||||||
|
f"Unexpected arguments for 'logging show' command; got {len(argv)}, expected 2 or 3",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if sub == 'set':
|
||||||
|
if len(argv) < 3:
|
||||||
|
return _nul_out(
|
||||||
|
[
|
||||||
|
'ERR',
|
||||||
|
"Unexpected arguments for 'logging set' command; got 2, expected 3 or 4",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if len(argv) > 4:
|
||||||
|
return _nul_out(
|
||||||
|
[
|
||||||
|
'ERR',
|
||||||
|
f"Unexpected arguments for 'logging set' command; got {len(argv)}, expected 3 or 4",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_cmd_parser() -> argparse.ArgumentParser:
|
||||||
|
"""Parse `config` / `logging` subcommands; trailing tokens go in `extras` where listed."""
|
||||||
|
kw: dict = {'prog': 'x52ctl', 'add_help': False}
|
||||||
|
if sys.version_info >= (3, 9):
|
||||||
|
kw['exit_on_error'] = False
|
||||||
|
p = argparse.ArgumentParser(**kw)
|
||||||
|
sub = p.add_subparsers(dest='cmd', required=True)
|
||||||
|
|
||||||
|
pc = sub.add_parser('config')
|
||||||
|
pcs = pc.add_subparsers(dest='sub', required=True)
|
||||||
|
|
||||||
|
pr = pcs.add_parser('reload')
|
||||||
|
pr.add_argument('extras', nargs='*', default=[])
|
||||||
|
|
||||||
|
prs = pcs.add_parser('reset')
|
||||||
|
prs.add_argument('extras', nargs='*', default=[])
|
||||||
|
|
||||||
|
psa = pcs.add_parser('save')
|
||||||
|
psa.add_argument('extras', nargs='*', default=[])
|
||||||
|
|
||||||
|
pld = pcs.add_parser('load')
|
||||||
|
pld.add_argument('path')
|
||||||
|
pld.add_argument('extras', nargs='*', default=[])
|
||||||
|
|
||||||
|
pcl = pcs.add_parser('clear')
|
||||||
|
pcl.add_argument('target', choices=('state', 'sysconf'))
|
||||||
|
pcl.add_argument('extras', nargs='*', default=[])
|
||||||
|
|
||||||
|
pdu = pcs.add_parser('dump')
|
||||||
|
pdu.add_argument('path')
|
||||||
|
pdu.add_argument('extras', nargs='*', default=[])
|
||||||
|
|
||||||
|
pge = pcs.add_parser('get')
|
||||||
|
pge.add_argument('section')
|
||||||
|
pge.add_argument('option')
|
||||||
|
pge.add_argument('extras', nargs='*', default=[])
|
||||||
|
|
||||||
|
pst = pcs.add_parser('set')
|
||||||
|
pst.add_argument('section')
|
||||||
|
pst.add_argument('option')
|
||||||
|
pst.add_argument('value')
|
||||||
|
pst.add_argument('extras', nargs='*', default=[])
|
||||||
|
|
||||||
|
pl = sub.add_parser('logging')
|
||||||
|
pls = pl.add_subparsers(dest='sub', required=True)
|
||||||
|
|
||||||
|
psh = pls.add_parser('show')
|
||||||
|
psh.add_argument('module', nargs='?', default=None)
|
||||||
|
psh.add_argument('extras', nargs='*', default=[])
|
||||||
|
|
||||||
|
pset = pls.add_parser('set')
|
||||||
|
pset.add_argument('tail', nargs='+', metavar='ARG')
|
||||||
|
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
_CMD_PARSER: argparse.ArgumentParser | None = None
|
||||||
|
|
||||||
|
_CMD_PARSE_EXC: tuple[type[BaseException], ...] = (SystemExit,)
|
||||||
|
if sys.version_info >= (3, 9):
|
||||||
|
_CMD_PARSE_EXC += (argparse.ArgumentError,)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_parser() -> argparse.ArgumentParser:
|
||||||
|
global _CMD_PARSER
|
||||||
|
if _CMD_PARSER is None:
|
||||||
|
_CMD_PARSER = _build_cmd_parser()
|
||||||
|
return _CMD_PARSER
|
||||||
|
|
||||||
|
|
||||||
|
def _dispatch_ns(fd: int, ns: argparse.Namespace) -> bytes:
|
||||||
|
if ns.cmd == 'config':
|
||||||
|
sub = ns.sub
|
||||||
|
if sub == 'reload':
|
||||||
|
return cmd_config_reload(fd)
|
||||||
|
if sub == 'reset':
|
||||||
|
return cmd_config_reset(fd)
|
||||||
|
if sub == 'save':
|
||||||
|
return cmd_config_save(fd)
|
||||||
|
if sub == 'load':
|
||||||
|
return cmd_config_load(fd, ns.path)
|
||||||
|
if sub == 'clear':
|
||||||
|
return cmd_config_clear(fd, ns.target)
|
||||||
|
if sub == 'dump':
|
||||||
|
return cmd_config_dump(fd, ns.path)
|
||||||
|
if sub == 'get':
|
||||||
|
return cmd_config_get(fd, ns.section, ns.option)
|
||||||
|
if sub == 'set':
|
||||||
|
return cmd_config_set(fd, ns.section, ns.option, ns.value)
|
||||||
|
if ns.cmd == 'logging':
|
||||||
|
if ns.sub == 'show':
|
||||||
|
return cmd_logging_show(fd, ns.module)
|
||||||
|
if ns.sub == 'set':
|
||||||
|
tail = ns.tail
|
||||||
|
if len(tail) == 1:
|
||||||
|
return cmd_logging_set(fd, None, tail[0])
|
||||||
|
return cmd_logging_set(fd, tail[0], tail[1])
|
||||||
|
return _nul_out(['ERR', f"Unknown command '{ns.cmd}'"])
|
||||||
|
|
||||||
|
|
||||||
|
def run_line(fd: int, argv: list[str]) -> bytes:
|
||||||
|
if not argv:
|
||||||
|
return b''
|
||||||
|
pre = _legacy_argc_errors(argv)
|
||||||
|
if pre:
|
||||||
|
return pre
|
||||||
|
try:
|
||||||
|
ns = _cmd_parser().parse_args(argv)
|
||||||
|
except _CMD_PARSE_EXC:
|
||||||
|
return _nul_out(['ERR', f"Unknown command '{argv[0]}'"])
|
||||||
|
if ns.cmd == 'logging' and ns.sub == 'set' and len(ns.tail) > 2:
|
||||||
|
return _nul_out(
|
||||||
|
[
|
||||||
|
'ERR',
|
||||||
|
f"Unexpected arguments for 'logging set' command; got {len(argv)}, expected 3 or 4",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return _dispatch_ns(fd, ns)
|
||||||
|
|
||||||
|
|
||||||
|
def run_interactive(fd: int) -> int:
|
||||||
|
sys.stdout.write('> ')
|
||||||
|
sys.stdout.flush()
|
||||||
|
for line in sys.stdin:
|
||||||
|
if line.strip().lower() == 'quit':
|
||||||
|
break
|
||||||
|
parts = shlex.split(line, comments=False)
|
||||||
|
if not parts:
|
||||||
|
sys.stdout.write('\n> ')
|
||||||
|
sys.stdout.flush()
|
||||||
|
continue
|
||||||
|
out = run_line(fd, parts)
|
||||||
|
sys.stdout.buffer.write(out)
|
||||||
|
sys.stdout.write('\n> ')
|
||||||
|
sys.stdout.flush()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _build_main_parser() -> argparse.ArgumentParser:
|
||||||
|
p = argparse.ArgumentParser(prog='x52ctl', add_help=True)
|
||||||
|
p.add_argument(
|
||||||
|
'-s',
|
||||||
|
'--socket',
|
||||||
|
metavar='PATH',
|
||||||
|
default=None,
|
||||||
|
help='path to the daemon framed IPC socket (default: compiled-in path)',
|
||||||
|
)
|
||||||
|
p.add_argument(
|
||||||
|
'-i',
|
||||||
|
'--interactive',
|
||||||
|
action='store_true',
|
||||||
|
help='read commands from stdin until EOF or quit',
|
||||||
|
)
|
||||||
|
p.add_argument(
|
||||||
|
'args',
|
||||||
|
nargs='*',
|
||||||
|
help='config … or logging … (see subcommands)',
|
||||||
|
)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
m = _build_main_parser()
|
||||||
|
a = m.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
fd = _open_ipc(a.socket)
|
||||||
|
except OSError as exc:
|
||||||
|
print(f'x52d_dial_ipc: {exc.strerror}', file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
if a.interactive:
|
||||||
|
if a.args:
|
||||||
|
print(
|
||||||
|
'Running in interactive mode, ignoring extra arguments',
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return run_interactive(fd)
|
||||||
|
if not a.args:
|
||||||
|
m.print_usage(file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
out = run_line(fd, a.args)
|
||||||
|
sys.stdout.buffer.write(out)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.close(fd)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
@ -20,6 +20,8 @@ def main():
|
||||||
include_guard = os.path.basename(sys.argv[1]).replace('-', '_').replace('.', '_').upper()
|
include_guard = os.path.basename(sys.argv[1]).replace('-', '_').replace('.', '_').upper()
|
||||||
print(f"#ifndef {include_guard}", file=out_fd)
|
print(f"#ifndef {include_guard}", file=out_fd)
|
||||||
print(f"#define {include_guard}\n", file=out_fd)
|
print(f"#define {include_guard}\n", file=out_fd)
|
||||||
|
print("#include <stdbool.h>", file=out_fd)
|
||||||
|
print("#include <stdint.h>\n", file=out_fd)
|
||||||
|
|
||||||
for mod in module_defs.Module:
|
for mod in module_defs.Module:
|
||||||
print(f"#define X52D_MOD_{mod.name} {mod.value}", file=out_fd)
|
print(f"#define X52D_MOD_{mod.name} {mod.value}", file=out_fd)
|
||||||
|
|
@ -32,6 +34,35 @@ def main():
|
||||||
print(f"int lookup_level_by_name(const char *name);", file=out_fd)
|
print(f"int lookup_level_by_name(const char *name);", file=out_fd)
|
||||||
print(f"const char * lookup_level_by_id(int id);", file=out_fd)
|
print(f"const char * lookup_level_by_id(int id);", file=out_fd)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"""
|
||||||
|
/** True if @p mod is a valid LIPC logging module selector: @c X52D_MOD_GLOBAL or @c 0 … @c X52D_MOD_MAX - 1. */
|
||||||
|
static inline bool x52d_module_wire_valid(uint16_t mod)
|
||||||
|
{
|
||||||
|
if (mod == X52D_MOD_GLOBAL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return mod < X52D_MOD_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if @p wire_u is a valid log level id for LIPC @c LOGGING_SET (same numeric space as @c loglevel_map / @c lookup_level_by_id).
|
||||||
|
* The wire field is unsigned; values such as @c FATAL (@c 0) and @c NOTSET (negative) are carried as two's-complement in @c uint64_t.
|
||||||
|
*/
|
||||||
|
static inline bool x52d_log_level_wire_valid(uint64_t wire_u)
|
||||||
|
{
|
||||||
|
int64_t wire = (int64_t)wire_u;
|
||||||
|
int v = (int)wire;
|
||||||
|
|
||||||
|
if ((int64_t)v != wire) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return lookup_level_by_id(v) != NULL;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
file=out_fd,
|
||||||
|
)
|
||||||
|
|
||||||
print(f"\n#endif // !defined {include_guard}", file=out_fd)
|
print(f"\n#endif // !defined {include_guard}", file=out_fd)
|
||||||
|
|
||||||
with open(sys.argv[2], 'w', encoding='utf-8') as out_fd:
|
with open(sys.argv[2], 'w', encoding='utf-8') as out_fd:
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,9 @@ communication protocol may break."""
|
||||||
print(f"#ifndef {include_guard}", file=out_fd)
|
print(f"#ifndef {include_guard}", file=out_fd)
|
||||||
print(f"#define {include_guard}", file=out_fd)
|
print(f"#define {include_guard}", file=out_fd)
|
||||||
print(file=out_fd)
|
print(file=out_fd)
|
||||||
|
print("#include <stdbool.h>", file=out_fd)
|
||||||
|
print("#include <stdint.h>", file=out_fd)
|
||||||
|
print(file=out_fd)
|
||||||
|
|
||||||
max_sec_val = max(self.sections.values()) + 1
|
max_sec_val = max(self.sections.values()) + 1
|
||||||
max_opt_val_global = 0
|
max_opt_val_global = 0
|
||||||
|
|
@ -131,19 +134,25 @@ communication protocol may break."""
|
||||||
|
|
||||||
print("extern const char * section_names[CFG_SECTION_MAX];", file=out_fd)
|
print("extern const char * section_names[CFG_SECTION_MAX];", file=out_fd)
|
||||||
print("extern const char * option_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL];", file=out_fd)
|
print("extern const char * option_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL];", file=out_fd)
|
||||||
|
print(file=out_fd)
|
||||||
|
print("/** True if (section_id, option_id) is a key from the build-time config registry.", file=out_fd)
|
||||||
|
print(" * Used by LIPC CONFIG_GET / CONFIG_SET to validate wire ids against config-defs.", file=out_fd)
|
||||||
|
print(" */", file=out_fd)
|
||||||
|
print("bool x52d_config_registry_pair_valid(uint16_t section_id, uint16_t option_id);", file=out_fd)
|
||||||
|
|
||||||
print(f"#endif // !defined {include_guard}", file=out_fd)
|
print(f"#endif // !defined {include_guard}", file=out_fd)
|
||||||
|
|
||||||
with open(output_source, 'w', encoding='utf-8') as out_fd:
|
with open(output_source, 'w', encoding='utf-8') as out_fd:
|
||||||
print("// Autogenerated config string table - DO NOT EDIT\n", file=out_fd)
|
print("// Autogenerated config string table - DO NOT EDIT\n", file=out_fd)
|
||||||
print(f'#include "{os.path.basename(output_header)}"', file=out_fd)
|
print(f'#include "{os.path.basename(output_header)}"', file=out_fd)
|
||||||
|
print("#include <stddef.h>", file=out_fd)
|
||||||
|
|
||||||
print("const char * section_names[CFG_SECTION_MAX] = {", file=out_fd)
|
print("const char * section_names[CFG_SECTION_MAX] = {", file=out_fd)
|
||||||
for section, value in self.sections.items():
|
for section, value in self.sections.items():
|
||||||
print(f' [{value}] = "{section.lower()}",', file=out_fd)
|
print(f' [{value}] = "{section.lower()}",', file=out_fd)
|
||||||
print("};\n", file=out_fd)
|
print("};\n", file=out_fd)
|
||||||
|
|
||||||
print("const char * options_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL] = {", file=out_fd)
|
print("const char * option_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL] = {", file=out_fd)
|
||||||
for section, value in self.sections.items():
|
for section, value in self.sections.items():
|
||||||
print(f' [{value}] =', '{', file=out_fd)
|
print(f' [{value}] =', '{', file=out_fd)
|
||||||
for option, value in self.options[section].items():
|
for option, value in self.options[section].items():
|
||||||
|
|
@ -151,6 +160,17 @@ communication protocol may break."""
|
||||||
print(' },', file=out_fd)
|
print(' },', file=out_fd)
|
||||||
print("};\n", file=out_fd)
|
print("};\n", file=out_fd)
|
||||||
|
|
||||||
|
print("bool x52d_config_registry_pair_valid(uint16_t section_id, uint16_t option_id)", file=out_fd)
|
||||||
|
print("{", file=out_fd)
|
||||||
|
print(" if (section_id < 1u || section_id >= CFG_SECTION_MAX) {", file=out_fd)
|
||||||
|
print(" return false;", file=out_fd)
|
||||||
|
print(" }", file=out_fd)
|
||||||
|
print(" if (option_id < 1u || option_id >= CFG_SECTION_MAX_OPT_VAL) {", file=out_fd)
|
||||||
|
print(" return false;", file=out_fd)
|
||||||
|
print(" }", file=out_fd)
|
||||||
|
print(" return option_names[section_id][option_id] != NULL;", file=out_fd)
|
||||||
|
print("}\n", file=out_fd)
|
||||||
|
|
||||||
|
|
||||||
def generate_py_definitions(self, output_file):
|
def generate_py_definitions(self, output_file):
|
||||||
"""Generate the Python definitions"""
|
"""Generate the Python definitions"""
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const char *x52d_command_sock_path(const char *sock_path);
|
||||||
int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote);
|
int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote);
|
||||||
const char *x52d_notify_sock_path(const char *sock_path);
|
const char *x52d_notify_sock_path(const char *sock_path);
|
||||||
int x52d_setup_notify_sock(const char *sock_path, struct sockaddr_un *remote);
|
int x52d_setup_notify_sock(const char *sock_path, struct sockaddr_un *remote);
|
||||||
|
const char *x52d_ipc_sock_path(const char *sock_path);
|
||||||
int x52d_set_socket_nonblocking(int sock_fd);
|
int x52d_set_socket_nonblocking(int sock_fd);
|
||||||
int x52d_listen_socket(struct sockaddr_un *local, int len, int sock_fd);
|
int x52d_listen_socket(struct sockaddr_un *local, int len, int sock_fd);
|
||||||
void x52d_split_args(int *argc, char **argv, char *buffer, int buflen);
|
void x52d_split_args(int *argc, char **argv, char *buffer, int buflen);
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ functions.
|
||||||
- libx52/libx52io.h
|
- libx52/libx52io.h
|
||||||
- libx52/libx52util.h
|
- libx52/libx52util.h
|
||||||
- vkm/vkm.h
|
- vkm/vkm.h
|
||||||
|
- libx52/x52d_ipc.h
|
||||||
- libx52/x52dcomm.h
|
- libx52/x52dcomm.h
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -638,6 +638,26 @@ LIBX52_API int libx52_set_blink(libx52_device *x52, uint8_t state);
|
||||||
* @{
|
* @{
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read USB vendor and product id of the connected device
|
||||||
|
*
|
||||||
|
* @param[out] vid Filled with USB idVendor when connected
|
||||||
|
* @param[out] pid Filled with USB idProduct when connected
|
||||||
|
*
|
||||||
|
* @returns \ref LIBX52_SUCCESS, or \ref LIBX52_ERROR_NO_DEVICE if not connected,
|
||||||
|
* or another \ref libx52_error_code on failure
|
||||||
|
*/
|
||||||
|
LIBX52_API int libx52_get_usb_ids(libx52_device *x52, uint16_t *vid, uint16_t *pid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief USB product string for the connected device
|
||||||
|
*
|
||||||
|
* The string is ASCII as returned by libusb. The pointer is valid until the
|
||||||
|
* device disconnects or @ref libx52_exit; it must not be freed. Returns an
|
||||||
|
* empty string when not connected or when the device has no product string.
|
||||||
|
*/
|
||||||
|
LIBX52_API const char *libx52_get_product_string(libx52_device *x52);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Update the X52
|
* @brief Update the X52
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,32 @@
|
||||||
* daemon communication library. These functions allow a client application to
|
* daemon communication library. These functions allow a client application to
|
||||||
* communicate with a running X52 daemon, execute commands and retrieve data.
|
* communicate with a running X52 daemon, execute commands and retrieve data.
|
||||||
*
|
*
|
||||||
|
* @par Primary API: framed LIPC (\c x52d.socket)
|
||||||
|
* Use @ref x52d_ipc_socket_path, @ref x52d_dial_ipc, and @ref x52d_ipc_call with
|
||||||
|
* opcodes and field semantics from @ref x52d_ipc.h. This is the **only** supported
|
||||||
|
* long-term path for daemon RPC and for **push** notifications on the same
|
||||||
|
* socket (frames with \c lipc_header.tid == \c 0), including **DEVICE_STATE**
|
||||||
|
* (USB connect/disconnect; see @ref proto_lipc_framed / @ref lipc_push_device_state
|
||||||
|
* in the daemon protocol documentation).
|
||||||
|
*
|
||||||
|
* @par Deprecated: legacy NUL sockets
|
||||||
|
* The helpers @ref x52d_dial_command, @ref x52d_format_command, @ref x52d_send_command,
|
||||||
|
* @ref x52d_dial_notify, and @ref x52d_recv_notification target the historical
|
||||||
|
* \c x52d.cmd and \c x52d.notify NUL-terminated protocols. They remain for
|
||||||
|
* migration only, are **deprecated**, and **will be removed** in a future release
|
||||||
|
* when those sockets are dropped. New code must use the framed IPC entry points
|
||||||
|
* above.
|
||||||
|
*
|
||||||
* @author Nirenjan Krishnan (nirenjan@nirenjan.org)
|
* @author Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||||
*/
|
*/
|
||||||
#ifndef X52DCOMM_H
|
#ifndef X52DCOMM_H
|
||||||
#define X52DCOMM_H
|
#define X52DCOMM_H
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <libx52/x52d_ipc.h>
|
||||||
|
#include <localipc/lipc.h>
|
||||||
|
|
||||||
#ifndef X52DCOMM_API
|
#ifndef X52DCOMM_API
|
||||||
# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
|
# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
|
||||||
|
|
@ -32,6 +52,21 @@
|
||||||
# endif
|
# endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define @c X52DCOMM_NO_DEPRECATED_ATTR before including this header to compile
|
||||||
|
* without @c deprecated attributes on legacy entry points (for in-tree sources
|
||||||
|
* that still call them until migration completes).
|
||||||
|
*/
|
||||||
|
#if defined(X52DCOMM_NO_DEPRECATED_ATTR)
|
||||||
|
# define X52DCOMM_DEPRECATED
|
||||||
|
#elif defined(__GNUC__) || defined(__clang__)
|
||||||
|
# define X52DCOMM_DEPRECATED __attribute__((deprecated("use x52d_ipc_socket_path, x52d_dial_ipc, and x52d_ipc_call")))
|
||||||
|
#elif defined(_MSC_VER)
|
||||||
|
# define X52DCOMM_DEPRECATED __declspec(deprecated("use x52d_ipc_socket_path, x52d_dial_ipc, and x52d_ipc_call"))
|
||||||
|
#else
|
||||||
|
# define X52DCOMM_DEPRECATED
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -44,94 +79,99 @@ extern "C" {
|
||||||
* @{
|
* @{
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resolve the framed IPC UNIX socket path used by x52d.
|
||||||
|
*
|
||||||
|
* If @p sock_path is NULL, returns the default path compiled into the library
|
||||||
|
* (same layout as the running daemon’s @c RUNDIR socket).
|
||||||
|
*
|
||||||
|
* @param[in] sock_path Optional override path, or NULL for the default.
|
||||||
|
* @return Pointer to a NUL-terminated path string (must not be freed).
|
||||||
|
*/
|
||||||
|
X52DCOMM_API const char *x52d_ipc_socket_path(const char *sock_path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Open a blocking connection to the daemon framed IPC socket.
|
||||||
|
*
|
||||||
|
* Use @ref x52d_ipc_call to issue requests. Close the descriptor with @c close(2)
|
||||||
|
* when finished.
|
||||||
|
*
|
||||||
|
* @param[in] sock_path Optional path override, or NULL for the default path from @ref x52d_ipc_socket_path.
|
||||||
|
* @returns Connected socket fd on success, or @c -1 with @c errno set.
|
||||||
|
*/
|
||||||
|
X52DCOMM_API int x52d_dial_ipc(const char *sock_path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Perform one synchronous framed IPC RPC on a connected fd.
|
||||||
|
*
|
||||||
|
* Allocates a short-lived liblocalipc client, sends one request, blocks until the
|
||||||
|
* matching reply (dispatching unrelated @c tid==0 pushes while waiting), then returns.
|
||||||
|
* Unrelated notifications are accepted and ignored unless liblocalipc is extended
|
||||||
|
* later with explicit dispatch hooks.
|
||||||
|
*
|
||||||
|
* @param[in] fd Connected stream socket from @ref x52d_dial_ipc.
|
||||||
|
* @param[in] request_id Wire @c request opcode (e.g. @c X52D_IPC_CONFIG_DUMP).
|
||||||
|
* @param[in] index @c lipc_header.index (opcode-specific).
|
||||||
|
* @param[in] value @c lipc_header.value (opcode-specific).
|
||||||
|
* @param[in] payload Request payload, or NULL when @p payload_len is 0.
|
||||||
|
* @param[in] payload_len Request payload length in bytes.
|
||||||
|
* @param[out] reply_hdr Decoded reply header, or NULL if not needed.
|
||||||
|
* @param[out] reply_payload Buffer for reply payload, or NULL when @p reply_payload_cap is 0.
|
||||||
|
* @param[in] reply_payload_cap Capacity of @p reply_payload.
|
||||||
|
* @param[out] reply_len Stored reply payload length; may be NULL.
|
||||||
|
*
|
||||||
|
* @return \c LIPC_OK when a reply was received and captured; other \c lipc_status
|
||||||
|
* values on protocol or I/O failure (\c errno may apply for \c LIPC_IO_ERROR).
|
||||||
|
*/
|
||||||
|
X52DCOMM_API lipc_status x52d_ipc_call(int fd, uint16_t request_id, uint16_t index, uint64_t value,
|
||||||
|
const void *payload, size_t payload_len,
|
||||||
|
lipc_header *reply_hdr, void *reply_payload, size_t reply_payload_cap, size_t *reply_len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Decode a DEVICE_STATE server push (@c X52D_IPC_PUSH_DEVICE_STATE, @c tid == 0).
|
||||||
|
*
|
||||||
|
* On success, @p connected is 1 when @c lipc_header.index indicates connected, else 0.
|
||||||
|
* @p name_utf8 and @p name_len describe the optional UTF-8 product substring in @p payload
|
||||||
|
* (not necessarily NUL-terminated; use @p name_len). Either name output may be NULL.
|
||||||
|
*
|
||||||
|
* @return 0 on success, -1 when @p hdr is not a well-formed DEVICE_STATE push.
|
||||||
|
*/
|
||||||
|
X52DCOMM_API int x52d_ipc_device_state_decode(const lipc_header *hdr, const void *payload,
|
||||||
|
size_t payload_len, int *connected, uint16_t *vid, uint16_t *pid, const char **name_utf8,
|
||||||
|
size_t *name_len);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Open a connection to the daemon command socket.
|
* @brief Open a connection to the daemon command socket.
|
||||||
*
|
*
|
||||||
* This method opens a socket connection to the daemon command socket. This
|
* @deprecated Legacy NUL-separated command socket; use @ref x52d_dial_ipc and
|
||||||
* socket allows the client to issue commands and retrieve data. The \p sock_path
|
* @ref x52d_ipc_call instead. This entry point will be removed when
|
||||||
* parameter may be NULL, in which case, it will use the default socket path.
|
* legacy sockets are dropped.
|
||||||
*
|
|
||||||
* The client will need to use the returned descriptor to communicate with the
|
|
||||||
* daemon using \ref x52d_send_command. Once finished, the client may use the
|
|
||||||
* \c close(2) method to close the file descriptor.
|
|
||||||
*
|
|
||||||
* @param[in] sock_path Path to the daemon command socket.
|
|
||||||
*
|
|
||||||
* @returns Non-negative socket file descriptor on success.
|
|
||||||
* @returns -1 on failure, and set \c errno accordingly.
|
|
||||||
*
|
|
||||||
* @exception E2BIG returned if the passed socket path is too big
|
|
||||||
*/
|
*/
|
||||||
X52DCOMM_API int x52d_dial_command(const char *sock_path);
|
X52DCOMM_API X52DCOMM_DEPRECATED int x52d_dial_command(const char *sock_path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Open a connection to the daemon notify socket.
|
* @brief Open a connection to the daemon notify socket.
|
||||||
*
|
*
|
||||||
* This method opens a socket connection to the daemon notify socket. This
|
* @deprecated Legacy NUL notify socket (\c x52d.notify). Subscribe on the unified
|
||||||
* socket allows the client to receive notifications from the daemon. Thej
|
* framed IPC socket (\ref x52d_dial_ipc) and handle \c tid==0 pushes
|
||||||
* \p sock_path parameter may be NULL, in which case, it will use the default
|
* (e.g. \c DEVICE_STATE; see @ref proto_lipc_framed). Removed in a future
|
||||||
* socket path.
|
* release with the legacy socket.
|
||||||
*
|
|
||||||
* The client will need to use the returned descriptor to communicate with the
|
|
||||||
* daemon using \ref x52d_recv_notification. Once finished, the client may use
|
|
||||||
* the \c close(2) method to close the file descriptor.
|
|
||||||
*
|
|
||||||
* @param[in] sock_path Path to the daemon command socket.
|
|
||||||
*
|
|
||||||
* @returns Non-negative socket file descriptor on success.
|
|
||||||
* @returns -1 on failure, and set \c errno accordingly.
|
|
||||||
*
|
|
||||||
* @exception E2BIG returned if the passed socket path is too big
|
|
||||||
*/
|
*/
|
||||||
X52DCOMM_API int x52d_dial_notify(const char *sock_path);
|
X52DCOMM_API X52DCOMM_DEPRECATED int x52d_dial_notify(const char *sock_path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Format a series of command strings into a buffer
|
* @brief Format a series of command strings into a buffer
|
||||||
*
|
*
|
||||||
* The client sends the command and parameters as a series of NUL terminated
|
* @deprecated Legacy command encoding for @ref x52d_send_command; use @ref x52d_ipc_call.
|
||||||
* strings. This function concatenates the commands into a single buffer that
|
|
||||||
* can be passed to \ref x52d_send_command.
|
|
||||||
*
|
|
||||||
* \p buffer should be at least 1024 bytes long.
|
|
||||||
*
|
|
||||||
* @param[in] argc Number of arguments to fit in the buffer
|
|
||||||
* @param[in] argv Pointer to an array of arguments.
|
|
||||||
* @param[out] buffer Buffer to store the formatted command
|
|
||||||
* @param[in] buflen Length of the buffer
|
|
||||||
*
|
|
||||||
* @returns number of bytes in the formatted command
|
|
||||||
* @returns -1 on an error condition, and \c errno is set accordingly.
|
|
||||||
*/
|
*/
|
||||||
X52DCOMM_API int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen);
|
X52DCOMM_API X52DCOMM_DEPRECATED int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Send a command to the daemon and retrieve the response.
|
* @brief Send a command to the daemon and retrieve the response.
|
||||||
*
|
*
|
||||||
* The client sends the command and parameters as a series of NUL terminated
|
* @deprecated Legacy NUL-separated command protocol; use @ref x52d_ipc_call.
|
||||||
* strings, and retrieves the response in the same manner. Depending on the
|
|
||||||
* result, the return status is either a positive integer or -1, and \c errno
|
|
||||||
* is set accordingly.
|
|
||||||
*
|
|
||||||
* \p buffer should contain sufficient space to accomodate the returned
|
|
||||||
* response string.
|
|
||||||
*
|
|
||||||
* This is a blocking function and will not return until either a response is
|
|
||||||
* received from the server, or an exception condition occurs.
|
|
||||||
*
|
|
||||||
* @param[in] sock_fd Socket descriptor returned from
|
|
||||||
* \ref x52d_dial_command
|
|
||||||
*
|
|
||||||
* @param[inout] buffer Pointer to the string containing the command and
|
|
||||||
* parameters. This is also used to save the returned
|
|
||||||
* response.
|
|
||||||
*
|
|
||||||
* @param[in] bufin Length of the command in the input buffer
|
|
||||||
* @param[in] bufout Maximum length of the response
|
|
||||||
*
|
|
||||||
* @returns number of bytes returned from the server
|
|
||||||
* @returns -1 on an error condition, and \c errno is set accordingly.
|
|
||||||
*/
|
*/
|
||||||
X52DCOMM_API int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout);
|
X52DCOMM_API X52DCOMM_DEPRECATED int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Notification callback function type
|
* @brief Notification callback function type
|
||||||
|
|
@ -141,28 +181,13 @@ typedef int (* x52d_notify_callback_fn)(int argc, char **argv);
|
||||||
/**
|
/**
|
||||||
* @brief Receive a notification from the daemon
|
* @brief Receive a notification from the daemon
|
||||||
*
|
*
|
||||||
* This function blocks until it receives a notification from the daemon. Once
|
* @deprecated Legacy NUL notify protocol; use framed IPC (\c tid==0) pushes on the
|
||||||
* it receives a notification successfully, it will call the callback function
|
* same socket as @ref x52d_dial_ipc.
|
||||||
* with the arguments as string pointers. It will return the return value of
|
|
||||||
* the callback function, if it was called.
|
|
||||||
*
|
|
||||||
* This is a blocking function and will not return until either a notification
|
|
||||||
* is received from the server, or an exception condition occurs.
|
|
||||||
*
|
|
||||||
* @param[in] sock_fd Socket descriptor returned from
|
|
||||||
* \ref x52d_dial_notify
|
|
||||||
*
|
|
||||||
* @param[in] callback Pointer to the callback function
|
|
||||||
*
|
|
||||||
* @returns return code of the callback function on success
|
|
||||||
* @returns -1 on an error condition, and \c errno is set accordingly.
|
|
||||||
*/
|
*/
|
||||||
X52DCOMM_API int x52d_recv_notification(int sock_fd, x52d_notify_callback_fn callback);
|
X52DCOMM_API X52DCOMM_DEPRECATED int x52d_recv_notification(int sock_fd, x52d_notify_callback_fn callback);
|
||||||
|
|
||||||
/** @} */
|
/** @} */
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#endif // !defined X52DCOMM_H
|
#endif /* X52DCOMM_H */
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ install_headers(
|
||||||
'libx52/libx52.h',
|
'libx52/libx52.h',
|
||||||
'libx52/libx52io.h',
|
'libx52/libx52io.h',
|
||||||
'libx52/libx52util.h',
|
'libx52/libx52util.h',
|
||||||
|
'libx52/x52d_ipc.h',
|
||||||
'libx52/x52dcomm.h',
|
'libx52/x52dcomm.h',
|
||||||
subdir: 'libx52'
|
subdir: 'libx52'
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ set -e
|
||||||
doc_html="$1"
|
doc_html="$1"
|
||||||
mandir="$2"
|
mandir="$2"
|
||||||
|
|
||||||
WANTED_PAGES="man1/x52cli.1 man1/x52bugreport.1"
|
# Installed manual pages from the Doxygen man tree (see GENERATE_MAN in Doxyfile.in).
|
||||||
|
WANTED_PAGES="man1/x52cli.1 man1/x52bugreport.1 man1/x52d.1 man1/x52d_protocol.1 man1/x52dcomm.1 man1/proto_lipc_framed.1 man1/x52ctl.1"
|
||||||
|
|
||||||
if [ -d "$MESON_BUILD_ROOT/docs/html" ]; then
|
if [ -d "$MESON_BUILD_ROOT/docs/html" ]; then
|
||||||
mkdir -p "$MESON_INSTALL_DESTDIR_PREFIX/$doc_html"
|
mkdir -p "$MESON_INSTALL_DESTDIR_PREFIX/$doc_html"
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,9 @@ struct libx52_device {
|
||||||
|
|
||||||
libusb_hotplug_callback_handle hotplug_handle;
|
libusb_hotplug_callback_handle hotplug_handle;
|
||||||
int handle_registered;
|
int handle_registered;
|
||||||
|
|
||||||
|
/** USB product string (ASCII from libusb), valid while @c hdl is non-NULL */
|
||||||
|
char usb_product[256];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Flag bits */
|
/** Flag bits */
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,38 @@ bool libx52_is_connected(libx52_device *dev)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int libx52_get_usb_ids(libx52_device *dev, uint16_t *vid, uint16_t *pid)
|
||||||
|
{
|
||||||
|
struct libusb_device_descriptor desc;
|
||||||
|
libusb_device *udev;
|
||||||
|
|
||||||
|
if (!dev || !vid || !pid) {
|
||||||
|
return LIBX52_ERROR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
if (!dev->hdl) {
|
||||||
|
*vid = 0;
|
||||||
|
*pid = 0;
|
||||||
|
return LIBX52_ERROR_NO_DEVICE;
|
||||||
|
}
|
||||||
|
udev = libusb_get_device(dev->hdl);
|
||||||
|
if (!udev || libusb_get_device_descriptor(udev, &desc) != 0) {
|
||||||
|
*vid = 0;
|
||||||
|
*pid = 0;
|
||||||
|
return LIBX52_ERROR_USB_FAILURE;
|
||||||
|
}
|
||||||
|
*vid = desc.idVendor;
|
||||||
|
*pid = desc.idProduct;
|
||||||
|
return LIBX52_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *libx52_get_product_string(libx52_device *dev)
|
||||||
|
{
|
||||||
|
if (!dev || !dev->hdl) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return dev->usb_product;
|
||||||
|
}
|
||||||
|
|
||||||
int libx52_disconnect(libx52_device *dev)
|
int libx52_disconnect(libx52_device *dev)
|
||||||
{
|
{
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
|
|
@ -107,6 +139,7 @@ int libx52_disconnect(libx52_device *dev)
|
||||||
dev->hdl = NULL;
|
dev->hdl = NULL;
|
||||||
dev->flags = 0;
|
dev->flags = 0;
|
||||||
dev->handle_registered = 0;
|
dev->handle_registered = 0;
|
||||||
|
dev->usb_product[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
return LIBX52_SUCCESS;
|
return LIBX52_SUCCESS;
|
||||||
|
|
@ -147,6 +180,14 @@ int libx52_connect(libx52_device *dev)
|
||||||
}
|
}
|
||||||
|
|
||||||
dev->hdl = hdl;
|
dev->hdl = hdl;
|
||||||
|
dev->usb_product[0] = '\0';
|
||||||
|
if (desc.iProduct != 0) {
|
||||||
|
int plen = libusb_get_string_descriptor_ascii(hdl, desc.iProduct,
|
||||||
|
(unsigned char *)dev->usb_product, (int)sizeof(dev->usb_product));
|
||||||
|
if (plen < 0) {
|
||||||
|
dev->usb_product[0] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (libx52_device_is_x52pro(desc.idProduct)) {
|
if (libx52_device_is_x52pro(desc.idProduct)) {
|
||||||
set_bit(&(dev->flags), X52_FLAG_IS_PRO);
|
set_bit(&(dev->flags), X52_FLAG_IS_PRO);
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,25 @@ LIPC_API lipc_status lipc_server_stop(lipc_server *server);
|
||||||
*/
|
*/
|
||||||
LIPC_API lipc_status lipc_server_run(lipc_server *server);
|
LIPC_API lipc_status lipc_server_run(lipc_server *server);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast one server-initiated notify frame (\c tid == 0) to every connected client.
|
||||||
|
*
|
||||||
|
* Threading: safe to call from threads other than @ref lipc_server_run while the server
|
||||||
|
* is active. Snapshots client file descriptors under the server mutex; framed writes run
|
||||||
|
* without holding the mutex.
|
||||||
|
*
|
||||||
|
* @param request @c lipc_header.request for subscribers (match client notify route id).
|
||||||
|
* @param status Wire status (often @ref LIPC_OK for application pushes).
|
||||||
|
* @param index Method-specific 16-bit field.
|
||||||
|
* @param value Method-specific 64-bit field.
|
||||||
|
* @param payload Optional payload (NULL allowed when @p payload_len is 0).
|
||||||
|
* @return @ref LIPC_BAD_HEADER if @p server is NULL, @ref LIPC_BAD_LENGTH if @p payload_len
|
||||||
|
* is not representable as @c uint32_t, else @ref LIPC_OK (per-client write failures
|
||||||
|
* such as @ref LIPC_WOULD_BLOCK are ignored).
|
||||||
|
*/
|
||||||
|
LIPC_API lipc_status lipc_server_broadcast_notify(lipc_server *server, uint16_t request,
|
||||||
|
uint16_t status, uint16_t index, uint64_t value, const void *payload, size_t payload_len);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send one response frame echoing request @c tid and @c request from @p reply->request_hdr.
|
* Send one response frame echoing request @c tid and @c request from @p reply->request_hdr.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -378,6 +378,48 @@ static lipc_status lipc_server_drain_client_frames(lipc_server *server, size_t i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum { LIPC_BROADCAST_MAX_FDS = 256 };
|
||||||
|
|
||||||
|
lipc_status lipc_server_broadcast_notify(lipc_server *server, uint16_t request, uint16_t status,
|
||||||
|
uint16_t index, uint64_t value, const void *payload, size_t payload_len)
|
||||||
|
{
|
||||||
|
int fds[LIPC_BROADCAST_MAX_FDS];
|
||||||
|
size_t nfds;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return LIPC_BAD_HEADER;
|
||||||
|
}
|
||||||
|
if (payload_len > UINT32_MAX) {
|
||||||
|
return LIPC_BAD_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_lock(&server->lock);
|
||||||
|
nfds = server->nclients;
|
||||||
|
if (nfds > sizeof fds / sizeof fds[0]) {
|
||||||
|
pthread_mutex_unlock(&server->lock);
|
||||||
|
return LIPC_BAD_LENGTH;
|
||||||
|
}
|
||||||
|
for (i = 0; i < nfds; i++) {
|
||||||
|
fds[i] = server->clients[i].fd;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&server->lock);
|
||||||
|
|
||||||
|
lipc_header h = {
|
||||||
|
.request = request,
|
||||||
|
.status = status,
|
||||||
|
.index = index,
|
||||||
|
.tid = 0,
|
||||||
|
.length = (uint32_t)payload_len,
|
||||||
|
.value = value,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (i = 0; i < nfds; i++) {
|
||||||
|
(void)lipc_frame_write(fds[i], &h, payload, payload_len);
|
||||||
|
}
|
||||||
|
return LIPC_OK;
|
||||||
|
}
|
||||||
|
|
||||||
lipc_status lipc_server_run(lipc_server *server)
|
lipc_status lipc_server_run(lipc_server *server)
|
||||||
{
|
{
|
||||||
int listen_fd;
|
int listen_fd;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue