feat: Migrate daemon to use localipc library

lipc-refactor
nirenjan 2026-04-26 21:18:33 -07:00
parent b8f059a881
commit 4ab8da6680
38 changed files with 2380 additions and 357 deletions

View File

@ -988,8 +988,9 @@ FILE_PATTERNS = libx52.h \
libx52util.h \ libx52util.h \
vkm.h \ vkm.h \
x52_cli.c \ x52_cli.c \
daemon_control.c \ daemon/x52ctl.dox \
x52dcomm.h \ x52dcomm.h \
x52d_ipc.h \
*.dox *.dox
# The RECURSIVE tag can be used to specify whether or not subdirectories should # The RECURSIVE tag can be used to specify whether or not subdirectories should

View File

@ -15,7 +15,9 @@
#include <sys/un.h> #include <sys/un.h>
#include <unistd.h> #include <unistd.h>
#include <localipc/lipc.h>
#include <libx52/x52dcomm.h> #include <libx52/x52dcomm.h>
#include <libx52/x52d_ipc.h>
#include <daemon/x52dcomm-internal.h> #include <daemon/x52dcomm-internal.h>
static int _setup_socket(struct sockaddr_un *remote, int len) static int _setup_socket(struct sockaddr_un *remote, int len)
@ -71,6 +73,71 @@ int x52d_dial_notify(const char *sock_path)
return _setup_socket(&remote, len); return _setup_socket(&remote, len);
} }
const char *x52d_ipc_socket_path(const char *sock_path)
{
return x52d_ipc_sock_path(sock_path);
}
int x52d_dial_ipc(const char *sock_path)
{
const char *path = x52d_ipc_sock_path(sock_path);
int fd;
fd = lipc_socket_connect(path, LIPC_SOCKET_CLOEXEC);
return fd;
}
int x52d_ipc_device_state_decode(const lipc_header *hdr, const void *payload, size_t payload_len,
int *connected, uint16_t *vid, uint16_t *pid, const char **name_utf8, size_t *name_len)
{
if (!hdr) {
return -1;
}
if (payload_len > 0 && !payload) {
return -1;
}
if (hdr->tid != 0 || hdr->request != X52D_IPC_PUSH_DEVICE_STATE) {
return -1;
}
if (hdr->index > 1u) {
return -1;
}
if (hdr->length != (uint32_t)payload_len) {
return -1;
}
if (connected) {
*connected = (hdr->index == 1u) ? 1 : 0;
}
if (vid || pid) {
x52d_ipc_device_state_unpack_usb(hdr->value, vid, pid);
}
if (name_utf8) {
*name_utf8 = (payload_len > 0) ? (const char *)payload : NULL;
}
if (name_len) {
*name_len = payload_len;
}
return 0;
}
lipc_status x52d_ipc_call(int fd, uint16_t request_id, uint16_t index, uint64_t value,
const void *payload, size_t payload_len,
lipc_header *reply_hdr, void *reply_payload, size_t reply_payload_cap, size_t *reply_len)
{
lipc_client *client;
lipc_status st;
client = lipc_client_create(0, NULL, NULL);
if (!client) {
return LIPC_INTERNAL_ERROR;
}
st = lipc_client_call(client, fd, request_id, index, value, payload, payload_len,
reply_hdr, reply_payload, reply_payload_cap, reply_len);
lipc_client_destroy(client);
return st;
}
int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen) int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen)
{ {
int msglen; int msglen;

View File

@ -34,6 +34,15 @@ const char * x52d_notify_sock_path(const char *sock_path)
return sock_path; return sock_path;
} }
const char *x52d_ipc_sock_path(const char *sock_path)
{
if (sock_path == NULL) {
sock_path = X52D_SOCK_IPC;
}
return sock_path;
}
static int _setup_sockaddr(struct sockaddr_un *remote, const char *sock_path) static int _setup_sockaddr(struct sockaddr_un *remote, const char *sock_path)
{ {
int len; int len;

View File

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

View File

@ -8,6 +8,13 @@
#include "build-config.h" #include "build-config.h"
#include <errno.h> #include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define PINELOG_MODULE X52D_MOD_CONFIG #define PINELOG_MODULE X52D_MOD_CONFIG
#include "pinelog.h" #include "pinelog.h"
@ -15,6 +22,28 @@
#include <daemon/constants.h> #include <daemon/constants.h>
static struct x52d_config x52d_config; static struct x52d_config x52d_config;
static const char *ipc_save_path;
void x52d_config_set_ipc_save_path(const char *path)
{
ipc_save_path = path;
}
int x52d_config_save_session(void)
{
int rc;
if (ipc_save_path != NULL) {
rc = x52d_config_save_file(&x52d_config, ipc_save_path);
if (rc != 0) {
PINELOG_ERROR(_("Error %d saving configuration file: %s"),
rc, strerror(rc));
}
return rc;
}
return x52d_config_save_state_atomic();
}
void x52d_config_load(const char *cfg_file) void x52d_config_load(const char *cfg_file)
{ {
@ -110,3 +139,223 @@ void x52d_config_apply(void)
x52d_cfg_set_ ## section ## _ ## key(x52d_config . name); x52d_cfg_set_ ## section ## _ ## key(x52d_config . name);
#include <daemon/config.def> #include <daemon/config.def>
} }
static int mkdir_p(char *path)
{
char *p;
int saved_errno;
for (p = path + 1; *p != '\0'; p++) {
if (*p != '/') {
continue;
}
*p = '\0';
if (mkdir(path, 0755) != 0 && errno != EEXIST) {
saved_errno = errno;
*p = '/';
return saved_errno != 0 ? saved_errno : EIO;
}
*p = '/';
}
if (mkdir(path, 0755) != 0 && errno != EEXIST) {
return errno != 0 ? errno : EIO;
}
return 0;
}
static int ensure_dir_for_file(const char *filepath)
{
char buf[PATH_MAX];
char *slash;
if (filepath == NULL || filepath[0] == '\0') {
return EINVAL;
}
if (strlen(filepath) >= sizeof(buf)) {
return ENAMETOOLONG;
}
memcpy(buf, filepath, strlen(filepath) + 1);
slash = strrchr(buf, '/');
if (slash == NULL || slash == buf) {
return 0;
}
*slash = '\0';
return mkdir_p(buf);
}
int x52d_config_reload_canonical(void)
{
int rc;
const char *chosen = NULL;
rc = x52d_config_set_defaults(&x52d_config);
if (rc != 0) {
return rc;
}
if (access(X52D_STATE_CFG_FILE, R_OK) == 0) {
chosen = X52D_STATE_CFG_FILE;
} else if (access(X52D_SYS_CFG_FILE, R_OK) == 0) {
chosen = X52D_SYS_CFG_FILE;
}
if (chosen != NULL) {
rc = x52d_config_load_file(&x52d_config, chosen);
if (rc != 0) {
return rc;
}
}
rc = x52d_config_apply_overrides(&x52d_config);
x52d_config_clear_overrides();
return rc;
}
int x52d_config_load_from_path(const char *path)
{
int rc;
if (path == NULL || path[0] == '\0') {
return EINVAL;
}
if (access(path, R_OK) != 0) {
return errno != 0 ? errno : EACCES;
}
rc = x52d_config_set_defaults(&x52d_config);
if (rc != 0) {
return rc;
}
rc = x52d_config_load_file(&x52d_config, path);
if (rc != 0) {
return rc;
}
rc = x52d_config_apply_overrides(&x52d_config);
x52d_config_clear_overrides();
return rc;
}
int x52d_config_reset_to_defaults(void)
{
int rc;
rc = x52d_config_set_defaults(&x52d_config);
if (rc != 0) {
return rc;
}
rc = x52d_config_apply_overrides(&x52d_config);
x52d_config_clear_overrides();
return rc;
}
int x52d_config_dump_to_alloc(char **out, size_t *out_len)
{
FILE *fp;
int rc;
if (out == NULL || out_len == NULL) {
return EINVAL;
}
*out = NULL;
*out_len = 0;
fp = open_memstream(out, out_len);
if (fp == NULL) {
return errno != 0 ? errno : ENOMEM;
}
rc = x52d_config_write_ini(&x52d_config, fp, "(memory)");
if (fclose(fp) != 0) {
if (rc == 0) {
rc = errno != 0 ? errno : EIO;
}
free(*out);
*out = NULL;
*out_len = 0;
return rc;
}
return rc;
}
int x52d_config_save_state_atomic(void)
{
char template[PATH_MAX];
int fd;
FILE *fp;
int rc;
int errsv;
rc = ensure_dir_for_file(X52D_STATE_CFG_FILE);
if (rc != 0) {
return rc;
}
if (snprintf(template, sizeof template, "%s.XXXXXX", X52D_STATE_CFG_FILE) >= (int)sizeof(template)) {
return ENAMETOOLONG;
}
fd = mkstemp(template);
if (fd < 0) {
return errno != 0 ? errno : EIO;
}
fp = fdopen(fd, "w");
if (fp == NULL) {
errsv = errno;
close(fd);
unlink(template);
return errsv != 0 ? errsv : EIO;
}
rc = x52d_config_write_ini(&x52d_config, fp, template);
if (fflush(fp) != 0 && rc == 0) {
rc = errno != 0 ? errno : EIO;
}
if (fsync(fileno(fp)) != 0 && rc == 0) {
rc = errno != 0 ? errno : EIO;
}
if (fclose(fp) != 0 && rc == 0) {
rc = errno != 0 ? errno : EIO;
}
if (rc != 0) {
unlink(template);
return rc;
}
if (rename(template, X52D_STATE_CFG_FILE) != 0) {
errsv = errno;
unlink(template);
return errsv != 0 ? errsv : EIO;
}
return 0;
}
int x52d_config_clear_disk_then_reload(uint16_t target, int *out_unlink_errno)
{
const char *path = NULL;
int reload_rc;
if (out_unlink_errno != NULL) {
*out_unlink_errno = 0;
}
if (target == X52D_CONFIG_CLEAR_TARGET_STATE) {
path = X52D_STATE_CFG_FILE;
} else if (target == X52D_CONFIG_CLEAR_TARGET_SYSCONF) {
path = X52D_SYS_CFG_FILE;
} else {
return EINVAL;
}
if (unlink(path) != 0 && errno != ENOENT) {
if (out_unlink_errno != NULL) {
*out_unlink_errno = errno != 0 ? errno : EIO;
}
}
reload_rc = x52d_config_reload_canonical();
return reload_rc;
}

View File

@ -9,6 +9,7 @@
#ifndef X52D_CONFIG_H #ifndef X52D_CONFIG_H
#define X52D_CONFIG_H #define X52D_CONFIG_H
#include <stdio.h>
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <limits.h> #include <limits.h>
@ -106,9 +107,78 @@ void x52d_config_apply_immediate(const char *section, const char *key);
void x52d_config_apply(void); void x52d_config_apply(void);
int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file); int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file);
/** Write the full active configuration as INI to @p cfg_fp (for save or in-memory dump). */
int x52d_config_write_ini(struct x52d_config *cfg, FILE *cfg_fp, const char *path_label);
void x52d_config_save(const char *cfg_file); void x52d_config_save(const char *cfg_file);
int x52d_config_set(const char *section, const char *key, const char *value); int x52d_config_set(const char *section, const char *key, const char *value);
const char *x52d_config_get(const char *section, const char *key); const char *x52d_config_get(const char *section, const char *key);
/**
* Reload configuration using the canonical order: state file if present and readable,
* else system config if present, else in-memory defaults (plus CLI overrides once).
*
* @return 0 on success, or a positive errno-style code on failure.
*/
int x52d_config_reload_canonical(void);
/**
* Load defaults, then load @p path (must be readable). Applies CLI overrides.
*
* @return 0 on success, or a positive errno-style code on failure.
*/
int x52d_config_load_from_path(const char *path);
/**
* Reset active configuration to defaults and re-apply CLI overrides.
*
* @return 0 on success, or a positive errno-style code on failure.
*/
int x52d_config_reset_to_defaults(void);
/**
* Serialize the active configuration as INI text into a heap buffer.
* On success, @p *out is NUL-terminated (length includes the final NUL in @p *out_len).
*
* @return 0 on success, or a positive errno-style code on failure (@p *out undefined).
*/
int x52d_config_dump_to_alloc(char **out, size_t *out_len);
/**
* Atomically write the active configuration to @ref X52D_STATE_CFG_FILE
* (temp file in the same directory + rename).
*
* @return 0 on success, or a positive errno-style code on failure.
*/
int x52d_config_save_state_atomic(void);
/**
* When non-NULL, @ref x52d_config_save_session writes to this path (same file as @c x52d -c).
* When NULL, @ref x52d_config_save_state_atomic is used.
*/
void x52d_config_set_ipc_save_path(const char *path);
/**
* Save active configuration: session path if set, otherwise @ref x52d_config_save_state_atomic.
*
* @return 0 on success, or a positive errno-style code on failure.
*/
int x52d_config_save_session(void);
/**
* Delete on-disk configuration selected by LIPC @c CONFIG_CLEAR index, then run
* @ref x52d_config_reload_canonical. @p target is @ref X52D_CONFIG_CLEAR_TARGET_STATE or
* @ref X52D_CONFIG_CLEAR_TARGET_SYSCONF.
*
* Unlink uses @c ENOENT as success (nothing to remove). If removal fails (e.g. read-only
* sysconf), the errno is stored in @p out_unlink_errno when non-@c NULL and reload still runs.
*
* @return @c 0 if reload succeeded, else a positive errno-style code from reload.
* @param out_unlink_errno optional; set to @c 0 if unlink succeeded or file was absent;
* otherwise set to errno from @c unlink (2); reload is still attempted.
*/
int x52d_config_clear_disk_then_reload(uint16_t target, int *out_unlink_errno);
#endif // !defined X52D_CONFIG_H #endif // !defined X52D_CONFIG_H

View File

@ -93,11 +93,52 @@ static const char * date_format_dumper(const char *section, const char *key, str
#undef CHECK_PARAMS #undef CHECK_PARAMS
#undef CONFIG_PTR #undef CONFIG_PTR
int x52d_config_write_ini(struct x52d_config *cfg, FILE *cfg_fp, const char *path_label)
{
char *current_section = NULL;
const char *value = "";
int out_rc = 0;
if (cfg == NULL || cfg_fp == NULL || path_label == NULL) {
return EINVAL;
}
PINELOG_TRACE("Writing configuration INI to %s", path_label);
#define CFG(section, key, name, type, def) do { \
if (current_section == NULL || strcasecmp(current_section, #section)) { \
if (current_section != NULL) { \
free(current_section); \
} \
current_section = strdup(#section); \
if (current_section == NULL) { \
value = NULL; \
out_rc = ENOMEM; \
goto exit_write_ini; \
} \
PINELOG_TRACE("Printing section header %s", #section); \
fprintf(cfg_fp, "[%s]\n", #section); \
} \
PINELOG_TRACE("Dumping " #section "." #key " to %s", path_label); \
value = type ## _dumper(#section, #key, cfg, offsetof(struct x52d_config, name)); \
if (value == NULL) { \
PINELOG_ERROR(_("Failed to dump %s.%s to config stream %s"), \
#section, #key, path_label); \
out_rc = EIO; \
goto exit_write_ini; \
} \
fprintf(cfg_fp, "%s = %s\n", #key, value); \
} while (0);
#include <daemon/config.def>
exit_write_ini:
free(current_section);
return out_rc;
}
int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file) int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file)
{ {
FILE *cfg_fp; FILE *cfg_fp;
char *current_section = NULL; int rc;
const char *value;
if (cfg == NULL || cfg_file == NULL) { if (cfg == NULL || cfg_file == NULL) {
return EINVAL; return EINVAL;
@ -107,35 +148,14 @@ int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file)
if (cfg_fp == NULL) { if (cfg_fp == NULL) {
PINELOG_ERROR(_("Unable to save config file %s - code %d: %s"), PINELOG_ERROR(_("Unable to save config file %s - code %d: %s"),
cfg_file, errno, strerror(errno)); cfg_file, errno, strerror(errno));
return 1; return errno != 0 ? errno : EIO;
} }
PINELOG_TRACE("Saving configuration to file %s", cfg_file); rc = x52d_config_write_ini(cfg, cfg_fp, cfg_file);
#define CFG(section, key, name, type, def) do { \ if (fclose(cfg_fp) != 0 && rc == 0) {
if (current_section == NULL || strcasecmp(current_section, #section)) { \ rc = errno != 0 ? errno : EIO;
if (current_section != NULL) { \ }
free(current_section); \ return rc;
} \
current_section = strdup(#section); \
PINELOG_TRACE("Printing section header %s", #section); \
fprintf(cfg_fp, "[%s]\n", #section); \
} \
PINELOG_TRACE("Dumping " #section "." #key " to file %s", cfg_file); \
value = type ## _dumper(#section, #key, cfg, offsetof(struct x52d_config, name)); \
if (value == NULL) { \
PINELOG_ERROR(_("Failed to dump %s.%s to config file %s"), \
#section, #key, cfg_file); \
goto exit_dump; \
} else { \
fprintf(cfg_fp, "%s = %s\n", #key, value); \
} \
} while (0);
#include <daemon/config.def>
exit_dump:
free(current_section);
fclose(cfg_fp);
return (value == NULL);
} }
const char *x52d_config_get_param(struct x52d_config *cfg, const char *section, const char *key) const char *x52d_config_get_param(struct x52d_config *cfg, const char *section, const char *key)

View File

@ -9,17 +9,30 @@
#ifndef X52D_CONST_H #ifndef X52D_CONST_H
#define X52D_CONST_H #define X52D_CONST_H
#include <libx52/x52d_ipc.h>
#define X52D_APP_NAME "x52d" #define X52D_APP_NAME "x52d"
#define X52D_LOG_FILE LOGDIR "/" X52D_APP_NAME ".log" #define X52D_LOG_FILE LOGDIR "/" X52D_APP_NAME ".log"
#define X52D_SYS_CFG_FILE SYSCONFDIR "/" X52D_APP_NAME "/" X52D_APP_NAME ".conf" #define X52D_SYS_CFG_FILE SYSCONFDIR "/" X52D_APP_NAME "/" X52D_APP_NAME ".conf"
/** Persistent runtime configuration (not under ephemeral @c RUNDIR). */
#define X52D_STATE_CFG_FILE LOCALSTATEDIR "/lib/" X52D_APP_NAME "/" X52D_APP_NAME ".conf"
/** @deprecated Use @ref X52D_IPC_CONFIG_CLEAR_TARGET_STATE in @c libx52/x52d_ipc.h. */
#define X52D_CONFIG_CLEAR_TARGET_STATE X52D_IPC_CONFIG_CLEAR_TARGET_STATE
/** @deprecated Use @ref X52D_IPC_CONFIG_CLEAR_TARGET_SYSCONF in @c libx52/x52d_ipc.h. */
#define X52D_CONFIG_CLEAR_TARGET_SYSCONF X52D_IPC_CONFIG_CLEAR_TARGET_SYSCONF
#define X52D_PID_FILE RUNDIR "/" X52D_APP_NAME ".pid" #define X52D_PID_FILE RUNDIR "/" X52D_APP_NAME ".pid"
#define X52D_SOCK_COMMAND RUNDIR "/" X52D_APP_NAME ".cmd" #define X52D_SOCK_COMMAND RUNDIR "/" X52D_APP_NAME ".cmd"
#define X52D_SOCK_NOTIFY RUNDIR "/" X52D_APP_NAME ".notify" #define X52D_SOCK_NOTIFY RUNDIR "/" X52D_APP_NAME ".notify"
/** Framed IPC socket: RPC commands and async pushes share one path (legacy NUL \c .cmd / \c .notify remain until removal). */
#define X52D_SOCK_IPC RUNDIR "/" X52D_APP_NAME ".socket"
#include "gettext.h" #include "gettext.h"
#define N_(x) gettext_noop(x) #define N_(x) gettext_noop(x)
#define _(x) gettext(x) #define _(x) gettext(x)

View File

@ -9,6 +9,18 @@ the Windows X52 driver. It currently manages the following:
- MFD brightness - MFD brightness
- Clock display on MFD - Clock display on MFD
# Control and notification sockets
The \b supported control plane and event stream use \b liblocalipc framing on a
single UNIX stream socket (default basename \c x52d.socket under the runtime
directory). See \ref x52d_protocol and \ref proto_lipc_framed. Use \c -S to
override the socket path.
The historical \b NUL-separated command socket (\c x52d.cmd) and notify socket
(\c x52d.notify) remain for transitional callers; they are \b deprecated and
\b will be removed in a future release. Overrides use \c -s and \c -b. See
\ref x52d_protocol.
# Command line arguments # Command line arguments
- \c -f - Run daemon in foreground (default: no) - \c -f - Run daemon in foreground (default: no)
@ -18,8 +30,9 @@ the Windows X52 driver. It currently manages the following:
- \c -c - Path to configuration file - \c -c - Path to configuration file
- \c -p - Path to PID file - \c -p - Path to PID file
- \c -o - Configuration override - only applied during startup - \c -o - Configuration override - only applied during startup
- \c -s - Path to command socket (see \ref x52d_protocol) - \c -S - Path to the \b unified framed-IPC socket (RPC and \c tid==0 push notifications such as \c DEVICE_STATE; default \c x52d.socket under the runtime directory). \b This is the supported integration path (see \ref x52d_protocol).
- \c -b - Path to notify socket - \c -s - Path to the \b deprecated legacy NUL command socket (default \c x52d.cmd); \b will be removed once callers migrate to \c -S / LIPC.
- \c -b - Path to the \b deprecated legacy notify socket (default \c x52d.notify); \b will be removed in the same way.
# Configuration file # Configuration file

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

View File

@ -7,6 +7,7 @@
*/ */
#include "build-config.h" #include "build-config.h"
#include <string.h>
#include <unistd.h> #include <unistd.h>
#include <pthread.h> #include <pthread.h>
#include <stdbool.h> #include <stdbool.h>
@ -50,8 +51,14 @@ static void *x52_dev_thr(void *param)
sleep(DEV_ACQ_DELAY); sleep(DEV_ACQ_DELAY);
} else { } else {
/* Successfully connected */ /* Successfully connected */
uint16_t vid = 0;
uint16_t pid = 0;
const char *prod = "";
PINELOG_INFO(_("Device connected, writing configuration")); PINELOG_INFO(_("Device connected, writing configuration"));
X52D_NOTIFY("CONNECTED"); (void)libx52_get_usb_ids(x52_dev, &vid, &pid);
prod = libx52_get_product_string(x52_dev);
x52d_notify_device_state(1, vid, pid, prod, strlen(prod));
x52d_config_apply(); x52d_config_apply();
} }
} else { } else {
@ -171,11 +178,15 @@ int x52d_dev_update(void)
if (rc != LIBX52_SUCCESS) { if (rc != LIBX52_SUCCESS) {
if (rc == LIBX52_ERROR_NO_DEVICE) { if (rc == LIBX52_ERROR_NO_DEVICE) {
uint16_t vid = 0;
uint16_t pid = 0;
// Detach from the existing device, the next thread run will // Detach from the existing device, the next thread run will
// pick it up. // pick it up.
PINELOG_TRACE("Disconnecting detached device"); PINELOG_TRACE("Disconnecting detached device");
(void)libx52_get_usb_ids(x52_dev, &vid, &pid);
libx52_disconnect(x52_dev); libx52_disconnect(x52_dev);
X52D_NOTIFY("DISCONNECTED"); x52d_notify_device_state(0, vid, pid, NULL, 0);
} else { } else {
PINELOG_ERROR(_("Error %d when updating X52 device: %s"), PINELOG_ERROR(_("Error %d when updating X52 device: %s"),
rc, libx52_strerror(rc)); rc, libx52_strerror(rc));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,9 +25,9 @@
#include <daemon/mouse.h> #include <daemon/mouse.h>
#include <daemon/command.h> #include <daemon/command.h>
#include <daemon/notify.h> #include <daemon/notify.h>
#include <daemon/ipc_service.h>
#include <daemon/x52dcomm-internal.h> #include <daemon/x52dcomm-internal.h>
#include <daemon/keyboard_layout.h> #include <daemon/keyboard_layout.h>
#include <libx52/x52dcomm.h>
#include "pinelog.h" #include "pinelog.h"
static volatile int flag_quit; static volatile int flag_quit;
@ -96,7 +96,8 @@ static void usage(int exit_code)
"\t[-l log-file] [-o override]\n" "\t[-l log-file] [-o override]\n"
"\t[-c config-file] [-p pid-file]\n" "\t[-c config-file] [-p pid-file]\n"
"\t[-s command-socket-path]\n" "\t[-s command-socket-path]\n"
"\t[-b notify-socket-path]\n"), "\t[-b notify-socket-path]\n"
"\t[-S framed-ipc-socket-path]\n"),
X52D_APP_NAME); X52D_APP_NAME);
exit(exit_code); exit(exit_code);
} }
@ -214,6 +215,7 @@ int main(int argc, char **argv)
const char *pid_file = NULL; const char *pid_file = NULL;
const char *command_sock = NULL; const char *command_sock = NULL;
const char *notify_sock = NULL; const char *notify_sock = NULL;
const char *ipc_sock = NULL;
int opt; int opt;
int rc; int rc;
sigset_t sigblockset; sigset_t sigblockset;
@ -241,8 +243,9 @@ int main(int argc, char **argv)
* -p path to PID file (only used if running in background) * -p path to PID file (only used if running in background)
* -s path to command socket * -s path to command socket
* -b path to notify socket * -b path to notify socket
* -S path to framed IPC socket (commands and notifications)
*/ */
while ((opt = getopt(argc, argv, "fvql:o:c:p:s:b:h")) != -1) { while ((opt = getopt(argc, argv, "fvql:o:c:p:s:b:S:h")) != -1) {
switch (opt) { switch (opt) {
case 'f': case 'f':
foreground = true; foreground = true;
@ -291,6 +294,10 @@ int main(int argc, char **argv)
notify_sock = optarg; notify_sock = optarg;
break; break;
case 'S':
ipc_sock = optarg;
break;
case 'h': case 'h':
usage(EXIT_SUCCESS); usage(EXIT_SUCCESS);
break; break;
@ -301,6 +308,8 @@ int main(int argc, char **argv)
} }
} }
x52d_config_set_ipc_save_path(conf_file);
PINELOG_DEBUG(_("Foreground = %s"), foreground ? _("true") : _("false")); PINELOG_DEBUG(_("Foreground = %s"), foreground ? _("true") : _("false"));
PINELOG_DEBUG(_("Quiet = %s"), quiet ? _("true") : _("false")); PINELOG_DEBUG(_("Quiet = %s"), quiet ? _("true") : _("false"));
PINELOG_DEBUG(_("Verbosity = %d"), verbosity); PINELOG_DEBUG(_("Verbosity = %d"), verbosity);
@ -309,6 +318,7 @@ int main(int argc, char **argv)
PINELOG_DEBUG(_("PID file = %s"), pid_file); PINELOG_DEBUG(_("PID file = %s"), pid_file);
PINELOG_DEBUG(_("Command socket = %s"), command_sock); PINELOG_DEBUG(_("Command socket = %s"), command_sock);
PINELOG_DEBUG(_("Notify socket = %s"), notify_sock); PINELOG_DEBUG(_("Notify socket = %s"), notify_sock);
PINELOG_DEBUG(_("Framed IPC socket = %s"), ipc_sock);
start_daemon(foreground, pid_file); start_daemon(foreground, pid_file);
@ -330,6 +340,9 @@ int main(int argc, char **argv)
goto cleanup; goto cleanup;
} }
x52d_notify_init(notify_sock); x52d_notify_init(notify_sock);
if (x52d_ipc_init(ipc_sock) < 0) {
goto cleanup;
}
x52d_io_init(); x52d_io_init();
x52d_mouse_handler_init(); x52d_mouse_handler_init();
@ -350,14 +363,22 @@ int main(int argc, char **argv)
/* Check if we need to reload configuration */ /* Check if we need to reload configuration */
if (flag_reload) { if (flag_reload) {
PINELOG_INFO(_("Reloading X52 configuration")); PINELOG_INFO(_("Reloading X52 configuration"));
x52d_config_load(conf_file); if (x52d_config_reload_canonical() != 0) {
PINELOG_ERROR(_("Canonical configuration reload failed"));
}
x52d_config_apply(); x52d_config_apply();
flag_reload = false; flag_reload = false;
} }
if (flag_save_cfg) { if (flag_save_cfg) {
PINELOG_INFO(_("Saving X52 configuration to disk")); PINELOG_INFO(_("Saving X52 configuration to disk"));
x52d_config_save(conf_file); {
int save_rc = x52d_config_save_session();
if (save_rc != 0) {
PINELOG_ERROR(_("Error saving configuration: %s"), strerror(save_rc));
}
}
flag_save_cfg = false; flag_save_cfg = false;
} }
} }
@ -369,6 +390,7 @@ cleanup:
// Stop device threads // Stop device threads
x52d_clock_exit(); x52d_clock_exit();
x52d_dev_exit(); x52d_dev_exit();
x52d_ipc_exit();
x52d_command_exit(); x52d_command_exit();
x52d_notify_exit(); x52d_notify_exit();
x52d_mouse_handler_exit(); x52d_mouse_handler_exit();

View File

@ -22,6 +22,9 @@ module_defs = custom_target('module-defs',
command: [python, meson.current_source_dir() / 'x52d_gen_module.py', command: [python, meson.current_source_dir() / 'x52d_gen_module.py',
'@OUTPUT0@', '@OUTPUT1@']) '@OUTPUT0@', '@OUTPUT1@'])
# Header for generated config section/option ids (build tree).
dep_config_defs_gen = declare_dependency(sources: config_defs[0])
# Header only: ordering on module-map.h without compiling module-map.c per target. # Header only: ordering on module-map.h without compiling module-map.c per target.
dep_module_map_gen = declare_dependency(sources: module_defs[0]) dep_module_map_gen = declare_dependency(sources: module_defs[0])
@ -32,6 +35,8 @@ slib_comm_defs = static_library('x52dcommdefs',
'name-id-map.c', 'name-id-map.c',
) )
dep_threads = dependency('threads')
libx52dcomm_version = '1.0.0' libx52dcomm_version = '1.0.0'
libx52dcomm_sources = [ libx52dcomm_sources = [
@ -42,7 +47,7 @@ libx52dcomm_sources = [
root_includes = include_directories('..') root_includes = include_directories('..')
lib_libx52dcomm = library('x52dcomm', libx52dcomm_sources, lib_libx52dcomm = library('x52dcomm', libx52dcomm_sources,
dependencies: [dep_intl, dep_config_h, dep_module_map_gen], dependencies: [dep_intl, dep_config_h, dep_module_map_gen, dep_localipc, dep_threads],
version: libx52dcomm_version, version: libx52dcomm_version,
c_args: sym_hidden_cargs, c_args: sym_hidden_cargs,
install: true, install: true,
@ -52,10 +57,13 @@ pkgconfig.generate(lib_libx52dcomm,
name: 'x52dcomm', name: 'x52dcomm',
description: 'Client library for communicating with the x52d X52 daemon.', description: 'Client library for communicating with the x52d X52 daemon.',
version: libx52dcomm_version, version: libx52dcomm_version,
requires: ['localipc'],
) )
x52d_sources = [ x52d_sources = [
'main.c', 'main.c',
'ipc_service.c',
'ipc_lipc_handlers.c',
'config_parser.c', 'config_parser.c',
'config_dump.c', 'config_dump.c',
'config.c', 'config.c',
@ -74,12 +82,10 @@ x52d_sources = [
'crc32.c', 'crc32.c',
] ]
dep_threads = dependency('threads') # Comm sources are compiled into x52d (same as Autotools); libx52dcomm is for external clients and x52ctl (Python + ctypes).
# Comm sources are compiled into x52d (same as Autotools); libx52dcomm is only for x52ctl.
x52d_linkwith = [lib_libx52, lib_vkm, lib_libx52io, slib_comm_defs] x52d_linkwith = [lib_libx52, lib_vkm, lib_libx52io, slib_comm_defs]
x52d_deps = [dep_pinelog, dep_inih, dep_threads, dep_math, dep_intl, dep_config_h, x52d_deps = [dep_pinelog, dep_inih, dep_threads, dep_math, dep_intl, dep_config_h,
dep_module_map_gen] dep_module_map_gen, dep_config_defs_gen, dep_localipc]
x52d_cflags = [] x52d_cflags = []
exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources, exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources,
@ -89,11 +95,58 @@ exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources,
dependencies: x52d_deps, dependencies: x52d_deps,
link_with: x52d_linkwith) link_with: x52d_linkwith)
exe_x52ctl = executable('x52ctl', 'daemon_control.c', x52ctl_script = configure_file(
install: true, input: 'x52ctl.in',
dependencies: [dep_intl, dep_config_h, dep_module_map_gen], output: 'x52ctl',
include_directories: [includes, root_includes], configuration: {
link_with: lib_libx52dcomm) 'PYTHON': python.full_path(),
'X52D_PY_PKG': meson.current_build_dir(),
},
install: false,
)
x52d_python_install_dir = join_paths(get_option('datadir'), 'x52d', 'python')
x52ctl_pymain_abs = join_paths(get_option('prefix'), x52d_python_install_dir, 'x52ctl_main.py')
x52ctl_installed_sh = configure_file(
input: 'x52ctl-installed.in',
output: 'x52ctl.inst',
configuration: {
'PYTHON': '/usr/bin/env python3',
'X52CTL_PYMAIN': x52ctl_pymain_abs,
},
install: false,
)
x52ctl_buildpath = join_paths(meson.current_build_dir(), 'x52ctl')
x52ctl_test_dep = custom_target(
'x52ctl-test-dep',
command: [
find_program('sh'), '-c', 'chmod 755 "$1" && touch "$2"',
'_', x52ctl_buildpath, '@OUTPUT@',
],
output: 'x52ctl-test.stamp',
depend_files: [x52ctl_script],
)
install_data(
x52ctl_installed_sh,
install_dir: get_option('bindir'),
install_mode: 'rwxr-xr-x',
rename: 'x52ctl',
)
install_data(
files('command_defs.py', 'module_defs.py', 'x52ctl_main.py'),
install_dir: x52d_python_install_dir,
)
meson.add_install_script(
python,
files('install_copy_file.py'),
join_paths(meson.current_build_dir(), 'config_defs.py'),
join_paths(get_option('prefix'), x52d_python_install_dir, 'config_defs.py'),
)
install_data('x52d.conf', install_data('x52d.conf',
install_dir: join_paths(get_option('sysconfdir'), 'x52d')) install_dir: join_paths(get_option('sysconfdir'), 'x52d'))
@ -112,7 +165,7 @@ us_x52l = custom_target(
install_dir: join_paths(get_option('datadir'), 'x52d')) install_dir: join_paths(get_option('datadir'), 'x52d'))
test('daemon-communication', files('test_daemon_comm.py')[0], test('daemon-communication', files('test_daemon_comm.py')[0],
depends: [exe_x52d, exe_x52ctl], protocol: 'tap') depends: [exe_x52d, config_defs, x52ctl_test_dep], protocol: 'tap')
x52d_mouse_test_sources = ['mouse_test.c', 'mouse.c'] x52d_mouse_test_sources = ['mouse_test.c', 'mouse.c']
x52d_mouse_test = executable('x52d-mouse-test', x52d_mouse_test_sources, x52d_mouse_test = executable('x52d-mouse-test', x52d_mouse_test_sources,

View File

@ -14,9 +14,13 @@
#define PINELOG_MODULE X52D_MOD_NOTIFY #define PINELOG_MODULE X52D_MOD_NOTIFY
#include "pinelog.h" #include "pinelog.h"
#include <daemon/constants.h> #include <daemon/constants.h>
#include <daemon/ipc_service.h>
#include <daemon/notify.h> #include <daemon/notify.h>
#include <daemon/client.h> #include <daemon/client.h>
#include <libx52/x52d_ipc.h>
#define X52DCOMM_NO_DEPRECATED_ATTR 1
#include <libx52/x52dcomm.h> #include <libx52/x52dcomm.h>
#undef X52DCOMM_NO_DEPRECATED_ATTR
#include <daemon/x52dcomm-internal.h> #include <daemon/x52dcomm-internal.h>
static pthread_t notify_thr; static pthread_t notify_thr;
@ -107,6 +111,23 @@ static void * x52_notify_thr(void * param)
return NULL; return NULL;
} }
void x52d_notify_device_state(int connected, uint16_t vid, uint16_t pid, const char *product_utf8,
size_t product_len)
{
uint64_t val = x52d_ipc_device_state_pack_usb(vid, pid);
if (connected) {
X52D_NOTIFY("CONNECTED");
} else {
X52D_NOTIFY("DISCONNECTED");
}
if (product_utf8 == NULL) {
product_len = 0;
}
x52d_ipc_push_device_state(connected ? 1u : 0u, val, product_utf8, product_len);
}
void x52d_notify_send(int argc, const char **argv) void x52d_notify_send(int argc, const char **argv)
{ {
char buffer[X52D_BUFSZ + sizeof(uint16_t)]; char buffer[X52D_BUFSZ + sizeof(uint16_t)];

View File

@ -13,6 +13,16 @@ void x52d_notify_init(const char *notify_sock_path);
void x52d_notify_exit(void); void x52d_notify_exit(void);
void x52d_notify_send(int argc, const char **argv); void x52d_notify_send(int argc, const char **argv);
/**
* Emit legacy NUL notify plus framed IPC DEVICE_STATE.
* @param connected Non-zero when the device is connected, zero on disconnect.
* @param vid @c idVendor (meaningful when @p connected; on disconnect, pass last device ids).
* @param pid @c idProduct
* @param product_utf8 Optional product name (connect only); may be NULL when @p product_len is 0.
*/
void x52d_notify_device_state(int connected, uint16_t vid, uint16_t pid, const char *product_utf8,
size_t product_len);
#define X52D_NOTIFY(...) do { \ #define X52D_NOTIFY(...) do { \
const char *argv ## __LINE__ [] = {__VA_ARGS__}; \ const char *argv ## __LINE__ [] = {__VA_ARGS__}; \
x52d_notify_send(sizeof(argv ## __LINE__ )/sizeof(argv ## __LINE__ [0]), argv ## __LINE__ ); \ x52d_notify_send(sizeof(argv ## __LINE__ )/sizeof(argv ## __LINE__ [0]), argv ## __LINE__ ); \

View File

@ -1,24 +1,56 @@
/** /**
@page x52d_protocol X52 daemon socket communication protocol @page x52d_protocol X52 daemon control and notification protocols
The X52 daemon creates a Unix domain stream socket, by default at @section proto_supported Primary path: framed LIPC (liblocalipc)
`$(LOCALSTATEDIR)/run/x52d.cmd` and listens for connection requests from
clients at this location. This can be overridden by passing the -s flag when
starting the daemon.
# Protocol Overview The **supported** integration surface for daemon control and for **push**
notifications is **liblocalipc** on a single UNIX **stream** socket. By default
the daemon listens at `$(LOCALSTATEDIR)/run/x52d.socket` (same layout as other
runtime files under the configured runtime directory). The path is overridden
with the daemons \c -S option.
\b x52d requires that clients send it commands as a series of NUL terminated Clients should use \ref x52d_dial_ipc and \ref x52d_ipc_call from
\ref x52dcomm.h (and opcode / field definitions in \ref x52d_ipc.h). RPC
replies carry a non-zero transaction id (\c lipc_header.tid) matching the
request. **Server push** frames use \c tid == \c 0; see \ref proto_lipc_framed
and especially \ref lipc_push_device_state.
For a full description of opcodes, configuration save paths, reload order, and
logging wire ids, see @subpage proto_lipc_framed.
@section proto_deprecated Legacy NUL-separated sockets (deprecated)
The **legacy command** socket (\c x52d.cmd) and **legacy notify** socket
(\c x52d.notify) use a NUL-terminated “argv” style wire format. They remain
available **only during migration** of existing tools and scripts.
\b These legacy sockets and their wire format are \b deprecated and \b will be
\b removed in a future release. New code must use the framed LIPC socket
described above.
Default paths are under `$(LOCALSTATEDIR)/run/` (see \c X52D_SOCK_COMMAND and
\c X52D_SOCK_NOTIFY in the daemon). The daemon overrides them with \c -s
(command) and \c -b (notify).
The legacy client helpers in \ref x52dcomm.h (\c x52d_dial_command,
\c x52d_format_command, \c x52d_send_command, \c x52d_dial_notify,
\c x52d_recv_notification) are deprecated with the **same removal policy** as
the sockets. Use \c x52d_ipc_socket_path, \c x52d_dial_ipc, and \c x52d_ipc_call
instead.
@subsection proto_legacy_cmd Legacy NUL command protocol
\b x52d requires that clients send commands as a series of NUL terminated
strings, without any interleaving space. The command should be sent in a strings, without any interleaving space. The command should be sent in a
single `send` call, and the client may expect a response in a single `recv` single \c send call, and the client may expect a response in a single \c recv
call. call.
The `send` call must send exactly the number of bytes in the command text. The \c send call must send exactly the number of bytes in the command text.
Extra bytes will be treated as additional arguments, which would cause the Extra bytes will be treated as additional arguments, which would cause the
command to fail. It is recommended that the `recv` call uses a 1024 byte buffer command to fail. It is recommended that the \c recv call uses a 1024 byte buffer
to read the data. Responses will never exceed this length. to read the data. Responses will never exceed this length.
# Responses @subsubsection proto_legacy_resp Responses
The daemon sends the response as a series of NUL terminated strings, without The daemon sends the response as a series of NUL terminated strings, without
any interleaving space. The first string is always one of the following: any interleaving space. The first string is always one of the following:
@ -30,40 +62,52 @@ any interleaving space. The first string is always one of the following:
This determines whether the request was successful or not, and subsequent This determines whether the request was successful or not, and subsequent
strings describe the action, error or requested data. strings describe the action, error or requested data.
# Examples @subsubsection proto_legacy_ex Examples
## Reloading configuration
@par Reloading configuration
- \b send <tt>config\0reload\0</tt> - \b send <tt>config\0reload\0</tt>
- \b recv <tt>OK\0config\0reload\0</tt> - \b recv <tt>OK\0config\0reload\0</tt>
## Reading mouse speed @par Reading mouse speed
- \b send <tt>config\0get\0mouse\0speed\0</tt> - \b send <tt>config\0get\0mouse\0speed\0</tt>
- \b recv <tt>DATA\0mouse\0speed\010\0</tt> - \b recv <tt>DATA\0mouse\0speed\010\0</tt>
## Sending an invalid command @par Sending an invalid command
- \b send <tt>config reload</tt> - \b send <tt>config reload</tt>
- \b recv <tt>ERR\0Unknown command 'config reload'\0</tt> - \b recv <tt>ERR\0Unknown command 'config reload'\0</tt>
# Commands @subsection proto_legacy_vocab Legacy command vocabulary
\b x52d commands are arranged in a hierarchical fashion as follows: \b x52d commands are arranged in a hierarchical fashion as follows:
``` @code{.unparsed}
<command-group> [<sub-command-group> [<sub-command-group> [...]]] <command> [<arguments>] <command-group> [<sub-command-group> [<sub-command-group> [...]]] <command> [<arguments>]
``` @endcode
The list of supported commands are shown below: The list of supported **legacy NUL** commands are shown below (superseded on
the wire by @ref proto_lipc_framed where an opcode exists):
- @subpage proto_config - @subpage proto_config
- @subpage proto_logging - @subpage proto_logging
@subsection proto_legacy_notify Legacy notify socket (deprecated)
Subscribers connect to the legacy notify socket (\c x52d.notify by default).
The daemon broadcasts short NUL-framed messages (same packing style as the
legacy command responses) when internal events occur. Notably, device connect
and disconnect were historically reported as string events such as
\c CONNECTED and \c DISCONNECTED.
On the **framed LIPC** socket, the same information is carried by the
\c DEVICE_STATE push; see @ref lipc_push_device_state. Prefer subscribing on
the unified LIPC path before legacy removal.
*/ */
/** /**
@page proto_config Configuration management @page proto_config Configuration management (legacy NUL command socket)
@note This page documents the **deprecated** NUL-separated \c x52d.cmd protocol.
For new integrations use @ref proto_lipc_framed (\c CONFIG_* opcodes).
The \c config commands deal with \b x52d configuration subsystem, and have the The \c config commands deal with \b x52d configuration subsystem, and have the
following subcommands. following subcommands.
@ -223,7 +267,10 @@ ERR\0Error 22 setting 'led.fire'='none': Invalid argument\0
*/ */
/** /**
@page proto_logging Logging management @page proto_logging Logging management (legacy NUL command socket)
@note This page documents the **deprecated** NUL-separated \c x52d.cmd protocol.
For new integrations use @ref proto_lipc_framed (\c LOGGING_SHOW / \c LOGGING_SET).
The \c logging commands allow the user to fine tune the logging configuration The \c logging commands allow the user to fine tune the logging configuration
of \c x52d as well as adjust the log levels for either all the modules, or for of \c x52d as well as adjust the log levels for either all the modules, or for
@ -306,3 +353,110 @@ otherwise.
- <tt>\a module-name</tt> (if specified) - <tt>\a module-name</tt> (if specified)
- \a log-level - \a log-level
*/ */
/**
@page proto_lipc_framed Framed LIPC control and notify (x52d.socket)
The daemon exposes **liblocalipc** on a UNIX stream socket (default under
`$(LOCALSTATEDIR)/run/`, basename \c x52d.socket, overridable with \c -S). This
is the **only** supported long-term path for control-plane RPC and for **push**
notifications. Request opcodes for configuration and logging match
\ref x52d_ipc.h; field semantics and on-disk layout are summarized here.
@section lipc_push_device_state LIPC push: DEVICE_STATE (\c tid == 0)
When the USB device connects or disconnects, the daemon may emit a **push**
frame: \c lipc_header.tid is **zero**, and \c lipc_header.request identifies the
push type \c DEVICE_STATE, wire id \c X52D_IPC_PUSH_DEVICE_STATE (\c 0x8001) in
\ref x52d_ipc.h — distinct from RPC opcodes \c 0x01\c 0x08 and \c 0x11\c 0x12.
Semantics:
- \c lipc_header.index — **connection state**: \c 0 = disconnected, \c 1 = connected.
- \c lipc_header.value — USB identity in the **lower 32 bits**:
\c (uint32_t)((idVendor << 16) | idProduct) with 16-bit USB \c idVendor and
\c idProduct (upper bits reserved, zero unless extended later). On **connect**
(\c index == 1), this reflects the **current** device. On **disconnect**
(\c index == 0), the daemon **preserves the last connected** vendor/product id
so clients know **which** device dropped; if no device was ever connected in
this process lifetime, the lower 32 bits are \c 0.
- **Payload** — optional UTF-8. When **connected** (\c index == 1), the daemon
**may** include the USB **product** string (\c lipc_header.length > 0). Clients
must accept **zero-length** payload. When **disconnected**, payload is
normally empty.
- \c lipc_header.status — typically \c LIPC_OK for a well-formed push.
This push supersedes the legacy notify strings \c CONNECTED / \c DISCONNECTED
on the deprecated \c x52d.notify socket.
@section lipc_state_path Runtime configuration file
Persistent settings written by @c CONFIG_SAVE (and removed by @c CONFIG_CLEAR
when targeting state) live at:
@code
$(LOCALSTATEDIR)/lib/x52d/x52d.conf
@endcode
(\c X52D_STATE_CFG_FILE in daemon sources). The daemon writes this path with a temporary file in
the same directory followed by @c rename(2) so readers never see a torn file.
Static defaults shipped by the distribution use \c X52D_SYS_CFG_FILE under
@c $(SYSCONFDIR).
@section lipc_config_get_set CONFIG_GET and CONFIG_SET (registry ids)
@c CONFIG_SET (@c 0x07) and @c CONFIG_GET (@c 0x08) address a single configuration key using
numeric identifiers from the build-time registry (@c config_registry.json → @c config-defs.h /
@c config-defs.c), not free-form section/option strings on the wire:
- @c lipc_header.index — section id (e.g. @c CFG_SECTION_CLOCK, @c CFG_SECTION_LED, ...).
- @c lipc_header.value — option id within that section (e.g. @c CFG_OPTION_CLOCK_ENABLED).
The value must fit in 16 bits; garbage in the upper bits of the 64-bit wire field is rejected.
@c CONFIG_SET request payload is the new value as a UTF-8 string (length = @c lipc_header.length).
The daemon validates @c (index, value) with @c x52d_config_registry_pair_valid(), resolves names
via @c section_names / @c option_names, then applies the same parsing as file-based configuration
(@c x52d_config_set + @c x52d_config_apply_immediate).
@c CONFIG_GET uses the same @c index / @c value selection; the reply payload is a single string,
the same logical text as other dump paths (@c x52d_config_get / dumpers), not the legacy
NUL-separated @c DATA argv shape.
@section lipc_config_clear CONFIG_CLEAR targets
The LIPC @c CONFIG_CLEAR request selects a file via @c lipc_header.index:
- @c X52D_IPC_CONFIG_CLEAR_TARGET_STATE (@c 1): delete the runtime state file
(\ref lipc_state_path).
- @c X52D_IPC_CONFIG_CLEAR_TARGET_SYSCONF (@c 2): delete the system configuration file
(\c X52D_SYS_CFG_FILE under \c $(SYSCONFDIR)).
After attempting @c unlink(2), the daemon always runs the same reload sequence
as @c CONFIG_RELOAD: load the state file if it exists, else the sysconf file if
it exists, else defaults (then apply CLI overrides). If deleting the sysconf file
fails (e.g. read-only filesystem), the reply uses a non-success status with the
unlink error in the payload, but reload and apply still run so runtime state
stays consistent.
@section lipc_config_reload_order Canonical reload order
@c CONFIG_RELOAD and the post-@c CONFIG_CLEAR reload both prefer the state file
when present and readable, then the system configuration file, otherwise
in-memory defaults.
@section lipc_logging_show_set LOGGING_SHOW (@c 0x11) and LOGGING_SET (@c 0x12)
Logging control uses numeric ids from the same build-time tables as the legacy
NUL command path (@c module_defs.py → @c module-map.h / @c module-map.c and
@c loglevel_map / @c lookup_level_by_id). There are no module or level names on the LIPC wire.
- @c lipc_header.index — module id (@c X52D_MOD_* values, @c 0 … @c X52D_MOD_MAX - 1), or
@c X52D_MOD_GLOBAL (@c 0xFF) for the global log level. Clients must use @c x52d_module_wire_valid()
semantics (or the equivalent): any other @c index is invalid.
- @c lipc_header.value — for @c LOGGING_SET only, the log level id in the same numeric space as
@c lookup_level_by_id (including @c FATAL as @c 0 and negative ids such as @c NOTSET where present
in the map, encoded in the 64-bit field as two's-complement). Validated with @c x52d_log_level_wire_valid().
For @c LOGGING_SHOW, @c value must be zero (@c LIPC_FIELD_FORBIDDEN); the current level is read from pinelog.
@c LOGGING_SHOW reply payload is a single string (the level label, e.g. @c info), not the legacy @c DATA argv shape.
*/

View File

@ -49,7 +49,7 @@ class TestCase:
print(out) print(out)
cmd = [suite.find_control_program(), cmd = [suite.find_control_program(),
'-s', suite.command, '--', '-s', suite.ipc_socket,
*self.in_cmd] *self.in_cmd]
testcase = subprocess.run(cmd, stdout=subprocess.PIPE, check=False) testcase = subprocess.run(cmd, stdout=subprocess.PIPE, check=False)
@ -75,6 +75,7 @@ class Test:
self.tmpdir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with self.tmpdir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
self.command = os.path.join(self.tmpdir.name, "x52d.cmd") self.command = os.path.join(self.tmpdir.name, "x52d.cmd")
self.notify = os.path.join(self.tmpdir.name, "x52d.notify") self.notify = os.path.join(self.tmpdir.name, "x52d.notify")
self.ipc_socket = os.path.join(self.tmpdir.name, "x52d.socket")
self.daemon = None self.daemon = None
self.testcases = [] self.testcases = []
@ -128,6 +129,7 @@ class Test:
"-p", os.path.join(self.tmpdir.name, "x52d.pid"), # PID file "-p", os.path.join(self.tmpdir.name, "x52d.pid"), # PID file
"-s", self.command, # Command socket path "-s", self.command, # Command socket path
"-b", self.notify, # Notification socket path "-b", self.notify, # Notification socket path
"-S", self.ipc_socket, # Unified framed IPC socket path
] ]
# Create empty config file # Create empty config file

View File

@ -0,0 +1,3 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
exec "@PYTHON@" "@X52CTL_PYMAIN@" "$@"

42
daemon/x52ctl.dox 100644
View File

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

31
daemon/x52ctl.in 100644
View File

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

View File

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

View File

@ -20,6 +20,8 @@ def main():
include_guard = os.path.basename(sys.argv[1]).replace('-', '_').replace('.', '_').upper() include_guard = os.path.basename(sys.argv[1]).replace('-', '_').replace('.', '_').upper()
print(f"#ifndef {include_guard}", file=out_fd) print(f"#ifndef {include_guard}", file=out_fd)
print(f"#define {include_guard}\n", file=out_fd) print(f"#define {include_guard}\n", file=out_fd)
print("#include <stdbool.h>", file=out_fd)
print("#include <stdint.h>\n", file=out_fd)
for mod in module_defs.Module: for mod in module_defs.Module:
print(f"#define X52D_MOD_{mod.name} {mod.value}", file=out_fd) print(f"#define X52D_MOD_{mod.name} {mod.value}", file=out_fd)
@ -32,6 +34,35 @@ def main():
print(f"int lookup_level_by_name(const char *name);", file=out_fd) print(f"int lookup_level_by_name(const char *name);", file=out_fd)
print(f"const char * lookup_level_by_id(int id);", file=out_fd) print(f"const char * lookup_level_by_id(int id);", file=out_fd)
print(
"""
/** True if @p mod is a valid LIPC logging module selector: @c X52D_MOD_GLOBAL or @c 0 @c X52D_MOD_MAX - 1. */
static inline bool x52d_module_wire_valid(uint16_t mod)
{
if (mod == X52D_MOD_GLOBAL) {
return true;
}
return mod < X52D_MOD_MAX;
}
/**
* True if @p wire_u is a valid log level id for LIPC @c LOGGING_SET (same numeric space as @c loglevel_map / @c lookup_level_by_id).
* The wire field is unsigned; values such as @c FATAL (@c 0) and @c NOTSET (negative) are carried as two's-complement in @c uint64_t.
*/
static inline bool x52d_log_level_wire_valid(uint64_t wire_u)
{
int64_t wire = (int64_t)wire_u;
int v = (int)wire;
if ((int64_t)v != wire) {
return false;
}
return lookup_level_by_id(v) != NULL;
}
""",
file=out_fd,
)
print(f"\n#endif // !defined {include_guard}", file=out_fd) print(f"\n#endif // !defined {include_guard}", file=out_fd)
with open(sys.argv[2], 'w', encoding='utf-8') as out_fd: with open(sys.argv[2], 'w', encoding='utf-8') as out_fd:

View File

@ -113,6 +113,9 @@ communication protocol may break."""
print(f"#ifndef {include_guard}", file=out_fd) print(f"#ifndef {include_guard}", file=out_fd)
print(f"#define {include_guard}", file=out_fd) print(f"#define {include_guard}", file=out_fd)
print(file=out_fd) print(file=out_fd)
print("#include <stdbool.h>", file=out_fd)
print("#include <stdint.h>", file=out_fd)
print(file=out_fd)
max_sec_val = max(self.sections.values()) + 1 max_sec_val = max(self.sections.values()) + 1
max_opt_val_global = 0 max_opt_val_global = 0
@ -131,19 +134,25 @@ communication protocol may break."""
print("extern const char * section_names[CFG_SECTION_MAX];", file=out_fd) print("extern const char * section_names[CFG_SECTION_MAX];", file=out_fd)
print("extern const char * option_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL];", file=out_fd) print("extern const char * option_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL];", file=out_fd)
print(file=out_fd)
print("/** True if (section_id, option_id) is a key from the build-time config registry.", file=out_fd)
print(" * Used by LIPC CONFIG_GET / CONFIG_SET to validate wire ids against config-defs.", file=out_fd)
print(" */", file=out_fd)
print("bool x52d_config_registry_pair_valid(uint16_t section_id, uint16_t option_id);", file=out_fd)
print(f"#endif // !defined {include_guard}", file=out_fd) print(f"#endif // !defined {include_guard}", file=out_fd)
with open(output_source, 'w', encoding='utf-8') as out_fd: with open(output_source, 'w', encoding='utf-8') as out_fd:
print("// Autogenerated config string table - DO NOT EDIT\n", file=out_fd) print("// Autogenerated config string table - DO NOT EDIT\n", file=out_fd)
print(f'#include "{os.path.basename(output_header)}"', file=out_fd) print(f'#include "{os.path.basename(output_header)}"', file=out_fd)
print("#include <stddef.h>", file=out_fd)
print("const char * section_names[CFG_SECTION_MAX] = {", file=out_fd) print("const char * section_names[CFG_SECTION_MAX] = {", file=out_fd)
for section, value in self.sections.items(): for section, value in self.sections.items():
print(f' [{value}] = "{section.lower()}",', file=out_fd) print(f' [{value}] = "{section.lower()}",', file=out_fd)
print("};\n", file=out_fd) print("};\n", file=out_fd)
print("const char * options_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL] = {", file=out_fd) print("const char * option_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL] = {", file=out_fd)
for section, value in self.sections.items(): for section, value in self.sections.items():
print(f' [{value}] =', '{', file=out_fd) print(f' [{value}] =', '{', file=out_fd)
for option, value in self.options[section].items(): for option, value in self.options[section].items():
@ -151,6 +160,17 @@ communication protocol may break."""
print(' },', file=out_fd) print(' },', file=out_fd)
print("};\n", file=out_fd) print("};\n", file=out_fd)
print("bool x52d_config_registry_pair_valid(uint16_t section_id, uint16_t option_id)", file=out_fd)
print("{", file=out_fd)
print(" if (section_id < 1u || section_id >= CFG_SECTION_MAX) {", file=out_fd)
print(" return false;", file=out_fd)
print(" }", file=out_fd)
print(" if (option_id < 1u || option_id >= CFG_SECTION_MAX_OPT_VAL) {", file=out_fd)
print(" return false;", file=out_fd)
print(" }", file=out_fd)
print(" return option_names[section_id][option_id] != NULL;", file=out_fd)
print("}\n", file=out_fd)
def generate_py_definitions(self, output_file): def generate_py_definitions(self, output_file):
"""Generate the Python definitions""" """Generate the Python definitions"""

View File

@ -19,6 +19,7 @@ const char *x52d_command_sock_path(const char *sock_path);
int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote); int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote);
const char *x52d_notify_sock_path(const char *sock_path); const char *x52d_notify_sock_path(const char *sock_path);
int x52d_setup_notify_sock(const char *sock_path, struct sockaddr_un *remote); int x52d_setup_notify_sock(const char *sock_path, struct sockaddr_un *remote);
const char *x52d_ipc_sock_path(const char *sock_path);
int x52d_set_socket_nonblocking(int sock_fd); int x52d_set_socket_nonblocking(int sock_fd);
int x52d_listen_socket(struct sockaddr_un *local, int len, int sock_fd); int x52d_listen_socket(struct sockaddr_un *local, int len, int sock_fd);
void x52d_split_args(int *argc, char **argv, char *buffer, int buflen); void x52d_split_args(int *argc, char **argv, char *buffer, int buflen);

View File

@ -49,6 +49,7 @@ functions.
- libx52/libx52io.h - libx52/libx52io.h
- libx52/libx52util.h - libx52/libx52util.h
- vkm/vkm.h - vkm/vkm.h
- libx52/x52d_ipc.h
- libx52/x52dcomm.h - libx52/x52dcomm.h
*/ */

View File

@ -638,6 +638,26 @@ LIBX52_API int libx52_set_blink(libx52_device *x52, uint8_t state);
* @{ * @{
*/ */
/**
* @brief Read USB vendor and product id of the connected device
*
* @param[out] vid Filled with USB idVendor when connected
* @param[out] pid Filled with USB idProduct when connected
*
* @returns \ref LIBX52_SUCCESS, or \ref LIBX52_ERROR_NO_DEVICE if not connected,
* or another \ref libx52_error_code on failure
*/
LIBX52_API int libx52_get_usb_ids(libx52_device *x52, uint16_t *vid, uint16_t *pid);
/**
* @brief USB product string for the connected device
*
* The string is ASCII as returned by libusb. The pointer is valid until the
* device disconnects or @ref libx52_exit; it must not be freed. Returns an
* empty string when not connected or when the device has no product string.
*/
LIBX52_API const char *libx52_get_product_string(libx52_device *x52);
/** /**
* @brief Update the X52 * @brief Update the X52
* *

View File

@ -15,12 +15,32 @@
* daemon communication library. These functions allow a client application to * daemon communication library. These functions allow a client application to
* communicate with a running X52 daemon, execute commands and retrieve data. * communicate with a running X52 daemon, execute commands and retrieve data.
* *
* @par Primary API: framed LIPC (\c x52d.socket)
* Use @ref x52d_ipc_socket_path, @ref x52d_dial_ipc, and @ref x52d_ipc_call with
* opcodes and field semantics from @ref x52d_ipc.h. This is the **only** supported
* long-term path for daemon RPC and for **push** notifications on the same
* socket (frames with \c lipc_header.tid == \c 0), including **DEVICE_STATE**
* (USB connect/disconnect; see @ref proto_lipc_framed / @ref lipc_push_device_state
* in the daemon protocol documentation).
*
* @par Deprecated: legacy NUL sockets
* The helpers @ref x52d_dial_command, @ref x52d_format_command, @ref x52d_send_command,
* @ref x52d_dial_notify, and @ref x52d_recv_notification target the historical
* \c x52d.cmd and \c x52d.notify NUL-terminated protocols. They remain for
* migration only, are **deprecated**, and **will be removed** in a future release
* when those sockets are dropped. New code must use the framed IPC entry points
* above.
*
* @author Nirenjan Krishnan (nirenjan@nirenjan.org) * @author Nirenjan Krishnan (nirenjan@nirenjan.org)
*/ */
#ifndef X52DCOMM_H #ifndef X52DCOMM_H
#define X52DCOMM_H #define X52DCOMM_H
#include <stddef.h> #include <stddef.h>
#include <stdint.h>
#include <libx52/x52d_ipc.h>
#include <localipc/lipc.h>
#ifndef X52DCOMM_API #ifndef X52DCOMM_API
# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303 # if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
@ -32,6 +52,21 @@
# endif # endif
#endif #endif
/**
* Define @c X52DCOMM_NO_DEPRECATED_ATTR before including this header to compile
* without @c deprecated attributes on legacy entry points (for in-tree sources
* that still call them until migration completes).
*/
#if defined(X52DCOMM_NO_DEPRECATED_ATTR)
# define X52DCOMM_DEPRECATED
#elif defined(__GNUC__) || defined(__clang__)
# define X52DCOMM_DEPRECATED __attribute__((deprecated("use x52d_ipc_socket_path, x52d_dial_ipc, and x52d_ipc_call")))
#elif defined(_MSC_VER)
# define X52DCOMM_DEPRECATED __declspec(deprecated("use x52d_ipc_socket_path, x52d_dial_ipc, and x52d_ipc_call"))
#else
# define X52DCOMM_DEPRECATED
#endif
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
@ -44,94 +79,99 @@ extern "C" {
* @{ * @{
*/ */
/**
* @brief Resolve the framed IPC UNIX socket path used by x52d.
*
* If @p sock_path is NULL, returns the default path compiled into the library
* (same layout as the running daemons @c RUNDIR socket).
*
* @param[in] sock_path Optional override path, or NULL for the default.
* @return Pointer to a NUL-terminated path string (must not be freed).
*/
X52DCOMM_API const char *x52d_ipc_socket_path(const char *sock_path);
/**
* @brief Open a blocking connection to the daemon framed IPC socket.
*
* Use @ref x52d_ipc_call to issue requests. Close the descriptor with @c close(2)
* when finished.
*
* @param[in] sock_path Optional path override, or NULL for the default path from @ref x52d_ipc_socket_path.
* @returns Connected socket fd on success, or @c -1 with @c errno set.
*/
X52DCOMM_API int x52d_dial_ipc(const char *sock_path);
/**
* @brief Perform one synchronous framed IPC RPC on a connected fd.
*
* Allocates a short-lived liblocalipc client, sends one request, blocks until the
* matching reply (dispatching unrelated @c tid==0 pushes while waiting), then returns.
* Unrelated notifications are accepted and ignored unless liblocalipc is extended
* later with explicit dispatch hooks.
*
* @param[in] fd Connected stream socket from @ref x52d_dial_ipc.
* @param[in] request_id Wire @c request opcode (e.g. @c X52D_IPC_CONFIG_DUMP).
* @param[in] index @c lipc_header.index (opcode-specific).
* @param[in] value @c lipc_header.value (opcode-specific).
* @param[in] payload Request payload, or NULL when @p payload_len is 0.
* @param[in] payload_len Request payload length in bytes.
* @param[out] reply_hdr Decoded reply header, or NULL if not needed.
* @param[out] reply_payload Buffer for reply payload, or NULL when @p reply_payload_cap is 0.
* @param[in] reply_payload_cap Capacity of @p reply_payload.
* @param[out] reply_len Stored reply payload length; may be NULL.
*
* @return \c LIPC_OK when a reply was received and captured; other \c lipc_status
* values on protocol or I/O failure (\c errno may apply for \c LIPC_IO_ERROR).
*/
X52DCOMM_API lipc_status x52d_ipc_call(int fd, uint16_t request_id, uint16_t index, uint64_t value,
const void *payload, size_t payload_len,
lipc_header *reply_hdr, void *reply_payload, size_t reply_payload_cap, size_t *reply_len);
/**
* @brief Decode a DEVICE_STATE server push (@c X52D_IPC_PUSH_DEVICE_STATE, @c tid == 0).
*
* On success, @p connected is 1 when @c lipc_header.index indicates connected, else 0.
* @p name_utf8 and @p name_len describe the optional UTF-8 product substring in @p payload
* (not necessarily NUL-terminated; use @p name_len). Either name output may be NULL.
*
* @return 0 on success, -1 when @p hdr is not a well-formed DEVICE_STATE push.
*/
X52DCOMM_API int x52d_ipc_device_state_decode(const lipc_header *hdr, const void *payload,
size_t payload_len, int *connected, uint16_t *vid, uint16_t *pid, const char **name_utf8,
size_t *name_len);
/** /**
* @brief Open a connection to the daemon command socket. * @brief Open a connection to the daemon command socket.
* *
* This method opens a socket connection to the daemon command socket. This * @deprecated Legacy NUL-separated command socket; use @ref x52d_dial_ipc and
* socket allows the client to issue commands and retrieve data. The \p sock_path * @ref x52d_ipc_call instead. This entry point will be removed when
* parameter may be NULL, in which case, it will use the default socket path. * legacy sockets are dropped.
*
* The client will need to use the returned descriptor to communicate with the
* daemon using \ref x52d_send_command. Once finished, the client may use the
* \c close(2) method to close the file descriptor.
*
* @param[in] sock_path Path to the daemon command socket.
*
* @returns Non-negative socket file descriptor on success.
* @returns -1 on failure, and set \c errno accordingly.
*
* @exception E2BIG returned if the passed socket path is too big
*/ */
X52DCOMM_API int x52d_dial_command(const char *sock_path); X52DCOMM_API X52DCOMM_DEPRECATED int x52d_dial_command(const char *sock_path);
/** /**
* @brief Open a connection to the daemon notify socket. * @brief Open a connection to the daemon notify socket.
* *
* This method opens a socket connection to the daemon notify socket. This * @deprecated Legacy NUL notify socket (\c x52d.notify). Subscribe on the unified
* socket allows the client to receive notifications from the daemon. Thej * framed IPC socket (\ref x52d_dial_ipc) and handle \c tid==0 pushes
* \p sock_path parameter may be NULL, in which case, it will use the default * (e.g. \c DEVICE_STATE; see @ref proto_lipc_framed). Removed in a future
* socket path. * release with the legacy socket.
*
* The client will need to use the returned descriptor to communicate with the
* daemon using \ref x52d_recv_notification. Once finished, the client may use
* the \c close(2) method to close the file descriptor.
*
* @param[in] sock_path Path to the daemon command socket.
*
* @returns Non-negative socket file descriptor on success.
* @returns -1 on failure, and set \c errno accordingly.
*
* @exception E2BIG returned if the passed socket path is too big
*/ */
X52DCOMM_API int x52d_dial_notify(const char *sock_path); X52DCOMM_API X52DCOMM_DEPRECATED int x52d_dial_notify(const char *sock_path);
/** /**
* @brief Format a series of command strings into a buffer * @brief Format a series of command strings into a buffer
* *
* The client sends the command and parameters as a series of NUL terminated * @deprecated Legacy command encoding for @ref x52d_send_command; use @ref x52d_ipc_call.
* strings. This function concatenates the commands into a single buffer that
* can be passed to \ref x52d_send_command.
*
* \p buffer should be at least 1024 bytes long.
*
* @param[in] argc Number of arguments to fit in the buffer
* @param[in] argv Pointer to an array of arguments.
* @param[out] buffer Buffer to store the formatted command
* @param[in] buflen Length of the buffer
*
* @returns number of bytes in the formatted command
* @returns -1 on an error condition, and \c errno is set accordingly.
*/ */
X52DCOMM_API int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen); X52DCOMM_API X52DCOMM_DEPRECATED int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen);
/** /**
* @brief Send a command to the daemon and retrieve the response. * @brief Send a command to the daemon and retrieve the response.
* *
* The client sends the command and parameters as a series of NUL terminated * @deprecated Legacy NUL-separated command protocol; use @ref x52d_ipc_call.
* strings, and retrieves the response in the same manner. Depending on the
* result, the return status is either a positive integer or -1, and \c errno
* is set accordingly.
*
* \p buffer should contain sufficient space to accomodate the returned
* response string.
*
* This is a blocking function and will not return until either a response is
* received from the server, or an exception condition occurs.
*
* @param[in] sock_fd Socket descriptor returned from
* \ref x52d_dial_command
*
* @param[inout] buffer Pointer to the string containing the command and
* parameters. This is also used to save the returned
* response.
*
* @param[in] bufin Length of the command in the input buffer
* @param[in] bufout Maximum length of the response
*
* @returns number of bytes returned from the server
* @returns -1 on an error condition, and \c errno is set accordingly.
*/ */
X52DCOMM_API int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout); X52DCOMM_API X52DCOMM_DEPRECATED int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout);
/** /**
* @brief Notification callback function type * @brief Notification callback function type
@ -141,28 +181,13 @@ typedef int (* x52d_notify_callback_fn)(int argc, char **argv);
/** /**
* @brief Receive a notification from the daemon * @brief Receive a notification from the daemon
* *
* This function blocks until it receives a notification from the daemon. Once * @deprecated Legacy NUL notify protocol; use framed IPC (\c tid==0) pushes on the
* it receives a notification successfully, it will call the callback function * same socket as @ref x52d_dial_ipc.
* with the arguments as string pointers. It will return the return value of
* the callback function, if it was called.
*
* This is a blocking function and will not return until either a notification
* is received from the server, or an exception condition occurs.
*
* @param[in] sock_fd Socket descriptor returned from
* \ref x52d_dial_notify
*
* @param[in] callback Pointer to the callback function
*
* @returns return code of the callback function on success
* @returns -1 on an error condition, and \c errno is set accordingly.
*/ */
X52DCOMM_API int x52d_recv_notification(int sock_fd, x52d_notify_callback_fn callback); X52DCOMM_API X52DCOMM_DEPRECATED int x52d_recv_notification(int sock_fd, x52d_notify_callback_fn callback);
/** @} */ /** @} */
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
#endif // !defined X52DCOMM_H #endif /* X52DCOMM_H */

View File

@ -33,6 +33,7 @@ install_headers(
'libx52/libx52.h', 'libx52/libx52.h',
'libx52/libx52io.h', 'libx52/libx52io.h',
'libx52/libx52util.h', 'libx52/libx52util.h',
'libx52/x52d_ipc.h',
'libx52/x52dcomm.h', 'libx52/x52dcomm.h',
subdir: 'libx52' subdir: 'libx52'
) )

View File

@ -6,7 +6,8 @@ set -e
doc_html="$1" doc_html="$1"
mandir="$2" mandir="$2"
WANTED_PAGES="man1/x52cli.1 man1/x52bugreport.1" # Installed manual pages from the Doxygen man tree (see GENERATE_MAN in Doxyfile.in).
WANTED_PAGES="man1/x52cli.1 man1/x52bugreport.1 man1/x52d.1 man1/x52d_protocol.1 man1/x52dcomm.1 man1/proto_lipc_framed.1 man1/x52ctl.1"
if [ -d "$MESON_BUILD_ROOT/docs/html" ]; then if [ -d "$MESON_BUILD_ROOT/docs/html" ]; then
mkdir -p "$MESON_INSTALL_DESTDIR_PREFIX/$doc_html" mkdir -p "$MESON_INSTALL_DESTDIR_PREFIX/$doc_html"

View File

@ -54,6 +54,9 @@ struct libx52_device {
libusb_hotplug_callback_handle hotplug_handle; libusb_hotplug_callback_handle hotplug_handle;
int handle_registered; int handle_registered;
/** USB product string (ASCII from libusb), valid while @c hdl is non-NULL */
char usb_product[256];
}; };
/** Flag bits */ /** Flag bits */

View File

@ -96,6 +96,38 @@ bool libx52_is_connected(libx52_device *dev)
return false; return false;
} }
int libx52_get_usb_ids(libx52_device *dev, uint16_t *vid, uint16_t *pid)
{
struct libusb_device_descriptor desc;
libusb_device *udev;
if (!dev || !vid || !pid) {
return LIBX52_ERROR_INVALID_PARAM;
}
if (!dev->hdl) {
*vid = 0;
*pid = 0;
return LIBX52_ERROR_NO_DEVICE;
}
udev = libusb_get_device(dev->hdl);
if (!udev || libusb_get_device_descriptor(udev, &desc) != 0) {
*vid = 0;
*pid = 0;
return LIBX52_ERROR_USB_FAILURE;
}
*vid = desc.idVendor;
*pid = desc.idProduct;
return LIBX52_SUCCESS;
}
const char *libx52_get_product_string(libx52_device *dev)
{
if (!dev || !dev->hdl) {
return "";
}
return dev->usb_product;
}
int libx52_disconnect(libx52_device *dev) int libx52_disconnect(libx52_device *dev)
{ {
if (!dev) { if (!dev) {
@ -107,6 +139,7 @@ int libx52_disconnect(libx52_device *dev)
dev->hdl = NULL; dev->hdl = NULL;
dev->flags = 0; dev->flags = 0;
dev->handle_registered = 0; dev->handle_registered = 0;
dev->usb_product[0] = '\0';
} }
return LIBX52_SUCCESS; return LIBX52_SUCCESS;
@ -147,6 +180,14 @@ int libx52_connect(libx52_device *dev)
} }
dev->hdl = hdl; dev->hdl = hdl;
dev->usb_product[0] = '\0';
if (desc.iProduct != 0) {
int plen = libusb_get_string_descriptor_ascii(hdl, desc.iProduct,
(unsigned char *)dev->usb_product, (int)sizeof(dev->usb_product));
if (plen < 0) {
dev->usb_product[0] = '\0';
}
}
if (libx52_device_is_x52pro(desc.idProduct)) { if (libx52_device_is_x52pro(desc.idProduct)) {
set_bit(&(dev->flags), X52_FLAG_IS_PRO); set_bit(&(dev->flags), X52_FLAG_IS_PRO);

View File

@ -449,6 +449,25 @@ LIPC_API lipc_status lipc_server_stop(lipc_server *server);
*/ */
LIPC_API lipc_status lipc_server_run(lipc_server *server); LIPC_API lipc_status lipc_server_run(lipc_server *server);
/**
* Broadcast one server-initiated notify frame (\c tid == 0) to every connected client.
*
* Threading: safe to call from threads other than @ref lipc_server_run while the server
* is active. Snapshots client file descriptors under the server mutex; framed writes run
* without holding the mutex.
*
* @param request @c lipc_header.request for subscribers (match client notify route id).
* @param status Wire status (often @ref LIPC_OK for application pushes).
* @param index Method-specific 16-bit field.
* @param value Method-specific 64-bit field.
* @param payload Optional payload (NULL allowed when @p payload_len is 0).
* @return @ref LIPC_BAD_HEADER if @p server is NULL, @ref LIPC_BAD_LENGTH if @p payload_len
* is not representable as @c uint32_t, else @ref LIPC_OK (per-client write failures
* such as @ref LIPC_WOULD_BLOCK are ignored).
*/
LIPC_API lipc_status lipc_server_broadcast_notify(lipc_server *server, uint16_t request,
uint16_t status, uint16_t index, uint64_t value, const void *payload, size_t payload_len);
/** /**
* Send one response frame echoing request @c tid and @c request from @p reply->request_hdr. * Send one response frame echoing request @c tid and @c request from @p reply->request_hdr.
* *

View File

@ -378,6 +378,48 @@ static lipc_status lipc_server_drain_client_frames(lipc_server *server, size_t i
} }
} }
enum { LIPC_BROADCAST_MAX_FDS = 256 };
lipc_status lipc_server_broadcast_notify(lipc_server *server, uint16_t request, uint16_t status,
uint16_t index, uint64_t value, const void *payload, size_t payload_len)
{
int fds[LIPC_BROADCAST_MAX_FDS];
size_t nfds;
size_t i;
if (!server) {
return LIPC_BAD_HEADER;
}
if (payload_len > UINT32_MAX) {
return LIPC_BAD_LENGTH;
}
pthread_mutex_lock(&server->lock);
nfds = server->nclients;
if (nfds > sizeof fds / sizeof fds[0]) {
pthread_mutex_unlock(&server->lock);
return LIPC_BAD_LENGTH;
}
for (i = 0; i < nfds; i++) {
fds[i] = server->clients[i].fd;
}
pthread_mutex_unlock(&server->lock);
lipc_header h = {
.request = request,
.status = status,
.index = index,
.tid = 0,
.length = (uint32_t)payload_len,
.value = value,
};
for (i = 0; i < nfds; i++) {
(void)lipc_frame_write(fds[i], &h, payload, payload_len);
}
return LIPC_OK;
}
lipc_status lipc_server_run(lipc_server *server) lipc_status lipc_server_run(lipc_server *server)
{ {
int listen_fd; int listen_fd;