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 \
|
||||
vkm.h \
|
||||
x52_cli.c \
|
||||
daemon_control.c \
|
||||
daemon/x52ctl.dox \
|
||||
x52dcomm.h \
|
||||
x52d_ipc.h \
|
||||
*.dox
|
||||
|
||||
# The RECURSIVE tag can be used to specify whether or not subdirectories should
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@
|
|||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <localipc/lipc.h>
|
||||
#include <libx52/x52dcomm.h>
|
||||
#include <libx52/x52d_ipc.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 msglen;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,15 @@ const char * x52d_notify_sock_path(const char *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)
|
||||
{
|
||||
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 <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
|
||||
#include "pinelog.h"
|
||||
|
|
@ -15,6 +22,28 @@
|
|||
#include <daemon/constants.h>
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -110,3 +139,223 @@ void x52d_config_apply(void)
|
|||
x52d_cfg_set_ ## section ## _ ## key(x52d_config . name);
|
||||
#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
|
||||
#define X52D_CONFIG_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <limits.h>
|
||||
|
|
@ -106,9 +107,78 @@ void x52d_config_apply_immediate(const char *section, const char *key);
|
|||
void x52d_config_apply(void);
|
||||
|
||||
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);
|
||||
|
||||
int x52d_config_set(const char *section, const char *key, const char *value);
|
||||
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
|
||||
|
|
|
|||
|
|
@ -93,11 +93,52 @@ static const char * date_format_dumper(const char *section, const char *key, str
|
|||
#undef CHECK_PARAMS
|
||||
#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)
|
||||
{
|
||||
FILE *cfg_fp;
|
||||
char *current_section = NULL;
|
||||
const char *value;
|
||||
int rc;
|
||||
|
||||
if (cfg == NULL || cfg_file == NULL) {
|
||||
return EINVAL;
|
||||
|
|
@ -107,35 +148,14 @@ int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file)
|
|||
if (cfg_fp == NULL) {
|
||||
PINELOG_ERROR(_("Unable to save config file %s - code %d: %s"),
|
||||
cfg_file, errno, strerror(errno));
|
||||
return 1;
|
||||
return errno != 0 ? errno : EIO;
|
||||
}
|
||||
|
||||
PINELOG_TRACE("Saving configuration to file %s", cfg_file);
|
||||
#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); \
|
||||
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);
|
||||
rc = x52d_config_write_ini(cfg, cfg_fp, cfg_file);
|
||||
if (fclose(cfg_fp) != 0 && rc == 0) {
|
||||
rc = errno != 0 ? errno : EIO;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
const char *x52d_config_get_param(struct x52d_config *cfg, const char *section, const char *key)
|
||||
|
|
|
|||
|
|
@ -9,17 +9,30 @@
|
|||
#ifndef X52D_CONST_H
|
||||
#define X52D_CONST_H
|
||||
|
||||
#include <libx52/x52d_ipc.h>
|
||||
|
||||
#define X52D_APP_NAME "x52d"
|
||||
|
||||
#define X52D_LOG_FILE LOGDIR "/" X52D_APP_NAME ".log"
|
||||
|
||||
#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_SOCK_COMMAND RUNDIR "/" X52D_APP_NAME ".cmd"
|
||||
#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"
|
||||
#define N_(x) gettext_noop(x)
|
||||
#define _(x) gettext(x)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,18 @@ the Windows X52 driver. It currently manages the following:
|
|||
- MFD brightness
|
||||
- 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
|
||||
|
||||
- \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 -p - Path to PID file
|
||||
- \c -o - Configuration override - only applied during startup
|
||||
- \c -s - Path to command socket (see \ref x52d_protocol)
|
||||
- \c -b - Path to notify socket
|
||||
- \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 -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <string.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <stdbool.h>
|
||||
|
|
@ -50,8 +51,14 @@ static void *x52_dev_thr(void *param)
|
|||
sleep(DEV_ACQ_DELAY);
|
||||
} else {
|
||||
/* Successfully connected */
|
||||
uint16_t vid = 0;
|
||||
uint16_t pid = 0;
|
||||
const char *prod = "";
|
||||
|
||||
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();
|
||||
}
|
||||
} else {
|
||||
|
|
@ -171,11 +178,15 @@ int x52d_dev_update(void)
|
|||
|
||||
if (rc != LIBX52_SUCCESS) {
|
||||
if (rc == LIBX52_ERROR_NO_DEVICE) {
|
||||
uint16_t vid = 0;
|
||||
uint16_t pid = 0;
|
||||
|
||||
// Detach from the existing device, the next thread run will
|
||||
// pick it up.
|
||||
PINELOG_TRACE("Disconnecting detached device");
|
||||
(void)libx52_get_usb_ids(x52_dev, &vid, &pid);
|
||||
libx52_disconnect(x52_dev);
|
||||
X52D_NOTIFY("DISCONNECTED");
|
||||
x52d_notify_device_state(0, vid, pid, NULL, 0);
|
||||
} else {
|
||||
PINELOG_ERROR(_("Error %d when updating X52 device: %s"),
|
||||
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/command.h>
|
||||
#include <daemon/notify.h>
|
||||
#include <daemon/ipc_service.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
#include <daemon/keyboard_layout.h>
|
||||
#include <libx52/x52dcomm.h>
|
||||
#include "pinelog.h"
|
||||
|
||||
static volatile int flag_quit;
|
||||
|
|
@ -96,7 +96,8 @@ static void usage(int exit_code)
|
|||
"\t[-l log-file] [-o override]\n"
|
||||
"\t[-c config-file] [-p pid-file]\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);
|
||||
exit(exit_code);
|
||||
}
|
||||
|
|
@ -214,6 +215,7 @@ int main(int argc, char **argv)
|
|||
const char *pid_file = NULL;
|
||||
const char *command_sock = NULL;
|
||||
const char *notify_sock = NULL;
|
||||
const char *ipc_sock = NULL;
|
||||
int opt;
|
||||
int rc;
|
||||
sigset_t sigblockset;
|
||||
|
|
@ -241,8 +243,9 @@ int main(int argc, char **argv)
|
|||
* -p path to PID file (only used if running in background)
|
||||
* -s path to command 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) {
|
||||
case 'f':
|
||||
foreground = true;
|
||||
|
|
@ -291,6 +294,10 @@ int main(int argc, char **argv)
|
|||
notify_sock = optarg;
|
||||
break;
|
||||
|
||||
case 'S':
|
||||
ipc_sock = optarg;
|
||||
break;
|
||||
|
||||
case 'h':
|
||||
usage(EXIT_SUCCESS);
|
||||
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(_("Quiet = %s"), quiet ? _("true") : _("false"));
|
||||
PINELOG_DEBUG(_("Verbosity = %d"), verbosity);
|
||||
|
|
@ -309,6 +318,7 @@ int main(int argc, char **argv)
|
|||
PINELOG_DEBUG(_("PID file = %s"), pid_file);
|
||||
PINELOG_DEBUG(_("Command socket = %s"), command_sock);
|
||||
PINELOG_DEBUG(_("Notify socket = %s"), notify_sock);
|
||||
PINELOG_DEBUG(_("Framed IPC socket = %s"), ipc_sock);
|
||||
|
||||
start_daemon(foreground, pid_file);
|
||||
|
||||
|
|
@ -330,6 +340,9 @@ int main(int argc, char **argv)
|
|||
goto cleanup;
|
||||
}
|
||||
x52d_notify_init(notify_sock);
|
||||
if (x52d_ipc_init(ipc_sock) < 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
x52d_io_init();
|
||||
x52d_mouse_handler_init();
|
||||
|
||||
|
|
@ -350,14 +363,22 @@ int main(int argc, char **argv)
|
|||
/* Check if we need to reload configuration */
|
||||
if (flag_reload) {
|
||||
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();
|
||||
flag_reload = false;
|
||||
}
|
||||
|
||||
if (flag_save_cfg) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -369,6 +390,7 @@ cleanup:
|
|||
// Stop device threads
|
||||
x52d_clock_exit();
|
||||
x52d_dev_exit();
|
||||
x52d_ipc_exit();
|
||||
x52d_command_exit();
|
||||
x52d_notify_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',
|
||||
'@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.
|
||||
dep_module_map_gen = declare_dependency(sources: module_defs[0])
|
||||
|
||||
|
|
@ -32,6 +35,8 @@ slib_comm_defs = static_library('x52dcommdefs',
|
|||
'name-id-map.c',
|
||||
)
|
||||
|
||||
dep_threads = dependency('threads')
|
||||
|
||||
libx52dcomm_version = '1.0.0'
|
||||
|
||||
libx52dcomm_sources = [
|
||||
|
|
@ -42,7 +47,7 @@ libx52dcomm_sources = [
|
|||
root_includes = include_directories('..')
|
||||
|
||||
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,
|
||||
c_args: sym_hidden_cargs,
|
||||
install: true,
|
||||
|
|
@ -52,10 +57,13 @@ pkgconfig.generate(lib_libx52dcomm,
|
|||
name: 'x52dcomm',
|
||||
description: 'Client library for communicating with the x52d X52 daemon.',
|
||||
version: libx52dcomm_version,
|
||||
requires: ['localipc'],
|
||||
)
|
||||
|
||||
x52d_sources = [
|
||||
'main.c',
|
||||
'ipc_service.c',
|
||||
'ipc_lipc_handlers.c',
|
||||
'config_parser.c',
|
||||
'config_dump.c',
|
||||
'config.c',
|
||||
|
|
@ -74,12 +82,10 @@ x52d_sources = [
|
|||
'crc32.c',
|
||||
]
|
||||
|
||||
dep_threads = dependency('threads')
|
||||
|
||||
# Comm sources are compiled into x52d (same as Autotools); libx52dcomm is only for x52ctl.
|
||||
# Comm sources are compiled into x52d (same as Autotools); libx52dcomm is for external clients and x52ctl (Python + ctypes).
|
||||
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,
|
||||
dep_module_map_gen]
|
||||
dep_module_map_gen, dep_config_defs_gen, dep_localipc]
|
||||
x52d_cflags = []
|
||||
|
||||
exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources,
|
||||
|
|
@ -89,11 +95,58 @@ exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources,
|
|||
dependencies: x52d_deps,
|
||||
link_with: x52d_linkwith)
|
||||
|
||||
exe_x52ctl = executable('x52ctl', 'daemon_control.c',
|
||||
install: true,
|
||||
dependencies: [dep_intl, dep_config_h, dep_module_map_gen],
|
||||
include_directories: [includes, root_includes],
|
||||
link_with: lib_libx52dcomm)
|
||||
x52ctl_script = configure_file(
|
||||
input: 'x52ctl.in',
|
||||
output: 'x52ctl',
|
||||
configuration: {
|
||||
'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_dir: join_paths(get_option('sysconfdir'), 'x52d'))
|
||||
|
|
@ -112,7 +165,7 @@ us_x52l = custom_target(
|
|||
install_dir: join_paths(get_option('datadir'), 'x52d'))
|
||||
|
||||
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 = executable('x52d-mouse-test', x52d_mouse_test_sources,
|
||||
|
|
|
|||
|
|
@ -14,9 +14,13 @@
|
|||
#define PINELOG_MODULE X52D_MOD_NOTIFY
|
||||
#include "pinelog.h"
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/ipc_service.h>
|
||||
#include <daemon/notify.h>
|
||||
#include <daemon/client.h>
|
||||
#include <libx52/x52d_ipc.h>
|
||||
#define X52DCOMM_NO_DEPRECATED_ATTR 1
|
||||
#include <libx52/x52dcomm.h>
|
||||
#undef X52DCOMM_NO_DEPRECATED_ATTR
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
|
||||
static pthread_t notify_thr;
|
||||
|
|
@ -107,6 +111,23 @@ static void * x52_notify_thr(void * param)
|
|||
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)
|
||||
{
|
||||
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_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 { \
|
||||
const char *argv ## __LINE__ [] = {__VA_ARGS__}; \
|
||||
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
|
||||
`$(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.
|
||||
@section proto_supported Primary path: framed LIPC (liblocalipc)
|
||||
|
||||
# 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
|
||||
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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
# Responses
|
||||
@subsubsection proto_legacy_resp Responses
|
||||
|
||||
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:
|
||||
|
|
@ -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
|
||||
strings describe the action, error or requested data.
|
||||
|
||||
# Examples
|
||||
|
||||
## Reloading configuration
|
||||
@subsubsection proto_legacy_ex Examples
|
||||
|
||||
@par Reloading configuration
|
||||
- \b send <tt>config\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 recv <tt>DATA\0mouse\0speed\010\0</tt>
|
||||
|
||||
## Sending an invalid command
|
||||
|
||||
@par Sending an invalid command
|
||||
- \b send <tt>config reload</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:
|
||||
|
||||
```
|
||||
@code{.unparsed}
|
||||
<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_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
|
||||
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
|
||||
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)
|
||||
- \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)
|
||||
|
||||
cmd = [suite.find_control_program(),
|
||||
'-s', suite.command, '--',
|
||||
'-s', suite.ipc_socket,
|
||||
*self.in_cmd]
|
||||
|
||||
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.command = os.path.join(self.tmpdir.name, "x52d.cmd")
|
||||
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.testcases = []
|
||||
|
||||
|
|
@ -128,6 +129,7 @@ class Test:
|
|||
"-p", os.path.join(self.tmpdir.name, "x52d.pid"), # PID file
|
||||
"-s", self.command, # Command socket path
|
||||
"-b", self.notify, # Notification socket path
|
||||
"-S", self.ipc_socket, # Unified framed IPC socket path
|
||||
]
|
||||
|
||||
# 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()
|
||||
print(f"#ifndef {include_guard}", 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:
|
||||
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"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)
|
||||
|
||||
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"#define {include_guard}", 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_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 * 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)
|
||||
|
||||
with open(output_source, 'w', encoding='utf-8') as 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("#include <stddef.h>", file=out_fd)
|
||||
|
||||
print("const char * section_names[CFG_SECTION_MAX] = {", file=out_fd)
|
||||
for section, value in self.sections.items():
|
||||
print(f' [{value}] = "{section.lower()}",', 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():
|
||||
print(f' [{value}] =', '{', file=out_fd)
|
||||
for option, value in self.options[section].items():
|
||||
|
|
@ -151,6 +160,17 @@ communication protocol may break."""
|
|||
print(' },', 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):
|
||||
"""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);
|
||||
const char *x52d_notify_sock_path(const char *sock_path);
|
||||
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_listen_socket(struct sockaddr_un *local, int len, int sock_fd);
|
||||
void x52d_split_args(int *argc, char **argv, char *buffer, int buflen);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ functions.
|
|||
- libx52/libx52io.h
|
||||
- libx52/libx52util.h
|
||||
- vkm/vkm.h
|
||||
- libx52/x52d_ipc.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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -15,12 +15,32 @@
|
|||
* daemon communication library. These functions allow a client application to
|
||||
* 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)
|
||||
*/
|
||||
#ifndef X52DCOMM_H
|
||||
#define X52DCOMM_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <libx52/x52d_ipc.h>
|
||||
#include <localipc/lipc.h>
|
||||
|
||||
#ifndef X52DCOMM_API
|
||||
# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
|
||||
|
|
@ -32,6 +52,21 @@
|
|||
# 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
|
||||
extern "C" {
|
||||
#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.
|
||||
*
|
||||
* This method opens a socket connection to the daemon command socket. This
|
||||
* socket allows the client to issue commands and retrieve data. The \p sock_path
|
||||
* parameter may be NULL, in which case, it will use the default socket path.
|
||||
*
|
||||
* 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
|
||||
* @deprecated Legacy NUL-separated command socket; use @ref x52d_dial_ipc and
|
||||
* @ref x52d_ipc_call instead. This entry point will be removed when
|
||||
* legacy sockets are dropped.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* This method opens a socket connection to the daemon notify socket. This
|
||||
* socket allows the client to receive notifications from the daemon. Thej
|
||||
* \p sock_path parameter may be NULL, in which case, it will use the default
|
||||
* socket path.
|
||||
*
|
||||
* 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
|
||||
* @deprecated Legacy NUL notify socket (\c x52d.notify). Subscribe on the unified
|
||||
* framed IPC socket (\ref x52d_dial_ipc) and handle \c tid==0 pushes
|
||||
* (e.g. \c DEVICE_STATE; see @ref proto_lipc_framed). Removed in a future
|
||||
* release with the legacy socket.
|
||||
*/
|
||||
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
|
||||
*
|
||||
* The client sends the command and parameters as a series of NUL terminated
|
||||
* 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.
|
||||
* @deprecated Legacy command encoding for @ref x52d_send_command; use @ref x52d_ipc_call.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* The client sends the command and parameters as a series of NUL terminated
|
||||
* 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.
|
||||
* @deprecated Legacy NUL-separated command protocol; use @ref x52d_ipc_call.
|
||||
*/
|
||||
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
|
||||
|
|
@ -141,28 +181,13 @@ typedef int (* x52d_notify_callback_fn)(int argc, char **argv);
|
|||
/**
|
||||
* @brief Receive a notification from the daemon
|
||||
*
|
||||
* This function blocks until it receives a notification from the daemon. Once
|
||||
* it receives a notification successfully, it will call the callback function
|
||||
* 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.
|
||||
* @deprecated Legacy NUL notify protocol; use framed IPC (\c tid==0) pushes on the
|
||||
* same socket as @ref x52d_dial_ipc.
|
||||
*/
|
||||
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
|
||||
}
|
||||
#endif
|
||||
#endif // !defined X52DCOMM_H
|
||||
|
||||
|
||||
#endif /* X52DCOMM_H */
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ install_headers(
|
|||
'libx52/libx52.h',
|
||||
'libx52/libx52io.h',
|
||||
'libx52/libx52util.h',
|
||||
'libx52/x52d_ipc.h',
|
||||
'libx52/x52dcomm.h',
|
||||
subdir: 'libx52'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ set -e
|
|||
doc_html="$1"
|
||||
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
|
||||
mkdir -p "$MESON_INSTALL_DESTDIR_PREFIX/$doc_html"
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ struct libx52_device {
|
|||
|
||||
libusb_hotplug_callback_handle hotplug_handle;
|
||||
int handle_registered;
|
||||
|
||||
/** USB product string (ASCII from libusb), valid while @c hdl is non-NULL */
|
||||
char usb_product[256];
|
||||
};
|
||||
|
||||
/** Flag bits */
|
||||
|
|
|
|||
|
|
@ -96,6 +96,38 @@ bool libx52_is_connected(libx52_device *dev)
|
|||
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)
|
||||
{
|
||||
if (!dev) {
|
||||
|
|
@ -107,6 +139,7 @@ int libx52_disconnect(libx52_device *dev)
|
|||
dev->hdl = NULL;
|
||||
dev->flags = 0;
|
||||
dev->handle_registered = 0;
|
||||
dev->usb_product[0] = '\0';
|
||||
}
|
||||
|
||||
return LIBX52_SUCCESS;
|
||||
|
|
@ -147,6 +180,14 @@ int libx52_connect(libx52_device *dev)
|
|||
}
|
||||
|
||||
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)) {
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
int listen_fd;
|
||||
|
|
|
|||
Loading…
Reference in New Issue