diff --git a/meson.build b/meson.build index 49509bce..91cdd62b 100644 --- a/meson.build +++ b/meson.build @@ -151,6 +151,12 @@ if not cc.has_header_symbol('limits.h', 'PATH_MAX', prefix: system_ext_define) configh_data.set('PATH_MAX', 4096) endif endif +if cc.get_argument_syntax() == 'msvc' + add_project_arguments( + cc.get_supported_arguments(['/experimental:c11atomics']), + language: 'c' + ) +endif # Silence some security & deprecation warnings on MSVC # for some unix/C functions we use. @@ -512,6 +518,17 @@ if build_tools c_args: ['-DENABLE_PRIVATE_APIS'], include_directories: [include_directories('src', 'include')], install: false) + thread_dep = dependency('threads', required: false) + if thread_dep.found() and cc.has_header('sys/un.h') + # Similar to previous tool, but for batch processing using a server + executable('compile-keymap-server', + 'tools/compile-keymap-server.c', + libxkbcommon_sources, + dependencies: [tools_dep, thread_dep], + c_args: ['-DENABLE_KEYMAP_SOCKET'], + include_directories: [include_directories('src', 'include')], + install: false) + endif # Tool: compose executable('xkbcli-compile-compose', diff --git a/src/atom.c b/src/atom.c index d43ac381..3d8f9138 100644 --- a/src/atom.c +++ b/src/atom.c @@ -80,6 +80,9 @@ #include "atom.h" #include "darray.h" #include "utils.h" +#ifdef ENABLE_KEYMAP_SOCKET +#include +#endif /* FNV-1a (http://www.isthe.com/chongo/tech/comp/fnv/). */ static inline uint32_t @@ -95,6 +98,7 @@ hash_buf(const char *string, size_t len) return hash; } +#ifndef ENABLE_KEYMAP_SOCKET /* * The atom table is an insert-only linear probing hash table * mapping strings to atoms. Another array maps the atoms to @@ -191,3 +195,108 @@ atom_intern(struct atom_table *table, const char *string, size_t len, bool add) assert(!"couldn't find an empty slot during probing"); } + +#else + +struct atom_table { + size_t size; + _Atomic(char *) *strings; +}; + +struct atom_table * +atom_table_new(size_t size_log2) +{ + struct atom_table *table = calloc(1, sizeof(*table)); + if (!table) + return NULL; + size_t size = 1u << size_log2; + table->size = size; + table->strings = calloc(size, sizeof(char*)); + return table; +} + +void +atom_table_free(struct atom_table *table) +{ + if (!table) + return; + + size_t count = 0; + for (size_t k = 0 ; k < table->size; k++) { + if (table->strings[k]) { + free(table->strings[k]); + count++; + } + } + free(table->strings); + free(table); +} + + +xkb_atom_t +atom_intern(struct atom_table *table, const char *string, size_t len, bool add) +{ + + uint32_t hash = hash_buf(string, len); + char *new_value = NULL; + for (size_t i = 0; i < table->size; i++) { + xkb_atom_t index_pos = (hash + i) & (table->size - 1); + if (index_pos == XKB_ATOM_NONE) + continue; + + char *existing_value = atomic_load_explicit(&table->strings[index_pos], + memory_order_acquire); + + /* Check if there is a value at the current index */ + if (!existing_value) { + /* No value defined. Check if we are allowed to add one */ + if (!add) + return XKB_ATOM_NONE; + + /* + * Prepare addition: duplicate string. + * Warning: This may not be our first attempt! + */ + if (!new_value) + new_value = strndup(string, len); + if (!new_value) { + /* Failed memory allocation */ + // FIXME: error handling? + return XKB_ATOM_NONE; + } + /* Try to register a new entry */ + if (atomic_compare_exchange_strong_explicit( + &table->strings[index_pos], &existing_value, new_value, + memory_order_release, memory_order_acquire)) { + return index_pos; + } + /* + * We were not fast enough to take the spot. + * But maybe the newly added value is our value, so read it again. + */ + existing_value = atomic_load_explicit(&table->strings[index_pos], + memory_order_acquire); + } + + /* Check the value are equal */ + if (strncmp(existing_value, string, len) == 0 && + existing_value[len] == '\0') { + /* We may have tried unsuccessfully to add the string */ + free(new_value); + return index_pos; + } + /* Hash collision: try next atom */ + } + + free(new_value); + assert(!"couldn't find an empty slot during probing"); +} + +const char * +atom_text(struct atom_table *table, xkb_atom_t atom) +{ + assert(atom < table->size); + return table->strings[atom]; +} + +#endif diff --git a/src/atom.h b/src/atom.h index 49478db9..c1c26104 100644 --- a/src/atom.h +++ b/src/atom.h @@ -30,8 +30,13 @@ typedef uint32_t xkb_atom_t; struct atom_table; +#ifdef ENABLE_KEYMAP_SOCKET +struct atom_table * +atom_table_new(size_t size); +#else struct atom_table * atom_table_new(void); +#endif void atom_table_free(struct atom_table *table); diff --git a/src/context.c b/src/context.c index bcec16c4..932d0025 100644 --- a/src/context.c +++ b/src/context.c @@ -29,6 +29,7 @@ #include #include #include +#include #include "xkbcommon/xkbcommon.h" #include "utils.h" @@ -189,7 +190,7 @@ xkb_context_include_path_get(struct xkb_context *ctx, unsigned int idx) XKB_EXPORT struct xkb_context * xkb_context_ref(struct xkb_context *ctx) { - ctx->refcnt++; + atomic_fetch_add(&ctx->refcnt, 1); return ctx; } @@ -200,7 +201,7 @@ xkb_context_ref(struct xkb_context *ctx) XKB_EXPORT void xkb_context_unref(struct xkb_context *ctx) { - if (!ctx || --ctx->refcnt > 0) + if (!ctx || atomic_fetch_sub(&ctx->refcnt, 1) > 1) return; free(ctx->x11_atom_cache); @@ -312,7 +313,12 @@ xkb_context_new(enum xkb_context_flags flags) return NULL; } +#ifdef ENABLE_KEYMAP_SOCKET + /* NOTE: Size should be adusted to deal with xkeyboard-config database */ + ctx->atom_table = atom_table_new(14); +#else ctx->atom_table = atom_table_new(); +#endif if (!ctx->atom_table) { xkb_context_unref(ctx); return NULL; diff --git a/src/context.h b/src/context.h index 3b513da5..0e2b3f0b 100644 --- a/src/context.h +++ b/src/context.h @@ -26,11 +26,15 @@ #ifndef CONTEXT_H #define CONTEXT_H +#include + +#include "xkbcommon/xkbcommon.h" #include "atom.h" #include "messages-codes.h" +#include "src/utils.h" struct xkb_context { - int refcnt; + atomic_int refcnt; ATTR_PRINTF(3, 0) void (*log_fn)(struct xkb_context *ctx, enum xkb_log_level level, diff --git a/test/xkeyboard-config-test.py.in b/test/xkeyboard-config-test.py.in index cc2c823f..ef6e4ba5 100755 --- a/test/xkeyboard-config-test.py.in +++ b/test/xkeyboard-config-test.py.in @@ -3,19 +3,26 @@ from __future__ import annotations import argparse +import ctypes import gzip import itertools +import json import multiprocessing import os +import socket import subprocess import sys +import tempfile +import traceback import xml.etree.ElementTree as ET from abc import ABCMeta, abstractmethod from dataclasses import dataclass from functools import partial from pathlib import Path +from time import sleep from typing import ( TYPE_CHECKING, + BinaryIO, ClassVar, Iterable, Iterator, @@ -55,7 +62,6 @@ class RMLVO: variant: str | None option: str | None - @property def __iter__(self) -> Iterator[str | None]: yield self.rules yield self.model @@ -97,15 +103,15 @@ class RMLVO: class Invocation(RMLVO, metaclass=ABCMeta): exitstatus: int = 77 # default to “skipped” error: str | None = None - keymap: str = "" + keymap: bytes = b"" command: str = "" # The fully compiled keymap def __str_iter(self) -> Iterator[str]: yield f"- rmlvo: {self.to_yaml(self.rmlvo)}" - yield f' cmd: "{self.escape(self.command)}"' + yield f" cmd: {json.dumps(self.command)}" yield f" status: {self.exitstatus}" if self.error: - yield f' error: "{self.escape(self.error.strip())}"' + yield f" error: {json.dumps(self.error.strip())}" def __str__(self) -> str: return "\n".join(self.__str_iter()) @@ -119,15 +125,11 @@ class Invocation(RMLVO, metaclass=ABCMeta): @staticmethod def to_yaml(xs: Iterable[tuple[str, str | int]]) -> str: - fields = ", ".join(f'{k}: "{v}"' for k, v in xs) + fields = ", ".join(f"{k}: {json.dumps(v)}" for k, v in xs) return f"{{ {fields} }}" - @staticmethod - def escape(s: str) -> str: - return s.replace('"', '\\"') - - def _write(self, fd: TextIO) -> None: - fd.write(f"// {self.to_yaml(self.rmlvo)}\n") + def _write(self, fd: BinaryIO) -> None: + fd.write(f"// {self.to_yaml(self.rmlvo)}\n".encode("utf-8")) fd.write(self.keymap) def _write_keymap(self, output_dir: Path, compress: int) -> None: @@ -138,18 +140,30 @@ class Invocation(RMLVO, metaclass=ABCMeta): keymap_file = output_dir / self.model / layout if compress: keymap_file = keymap_file.with_suffix(".gz") - with gzip.open( - keymap_file, "wt", compresslevel=compress, encoding="utf-8" - ) as fd: + with gzip.open(keymap_file, "wb", compresslevel=compress) as fd: self._write(fd) fd.close() else: - with keymap_file.open("wt", encoding="utf-8") as fd: + with keymap_file.open("wb") as fd: self._write(fd) + def _print_result(self, short: bool, verbose: bool) -> None: + if self.exitstatus != 0: + target = sys.stderr + else: + target = sys.stdout if verbose else None + + if target: + if short: + print("-", self.to_yaml(self.short), file=target) + else: + print(self, file=target) + @classmethod @abstractmethod - def run(cls, i: Self, output_dir: Path | None, compress: int) -> Self: ... + def run( + cls, i: Self, output_dir: Path | None, compress: int, *args, **kwargs + ) -> Self: ... @classmethod def run_all( @@ -161,7 +175,9 @@ class Invocation(RMLVO, metaclass=ABCMeta): verbose: bool, short: bool, progress_bar: ProgressBar[Iterable[Self]], + chunksize: int, compress: int, + **kwargs, ) -> bool: if keymap_output_dir: try: @@ -172,22 +188,19 @@ class Invocation(RMLVO, metaclass=ABCMeta): failed = False with multiprocessing.Pool(njobs) as p: - f = partial(cls.run, output_dir=keymap_output_dir, compress=compress) - results = p.imap_unordered(f, combos) + f = partial( + cls.run, + output_dir=keymap_output_dir, + compress=compress, + ) + results = p.imap_unordered(f, combos, chunksize=chunksize) for invocation in progress_bar( results, total=combos_count, file=sys.stdout ): if invocation.exitstatus != 0: failed = True - target = sys.stderr - else: - target = sys.stdout if verbose else None - if target: - if short: - print("-", cls.to_yaml(invocation.short), file=target) - else: - print(invocation, file=target) + invocation._print_result(short, verbose) return failed @@ -197,7 +210,14 @@ class XkbCompInvocation(Invocation): xkbcomp_args: ClassVar[tuple[str, ...]] = ("xkbcomp", "-xkb", "-", "-") @classmethod - def run(cls, i: Self, output_dir: Path | None, compress: int) -> Self: + def run( + cls, + i: Self, + output_dir: Path | None, + compress: int, + *args, + **kwargs, + ) -> Self: i._run(output_dir, compress) return i @@ -233,7 +253,7 @@ class XkbCompInvocation(Invocation): self.error = "failed to compile keymap" self.exitstatus = xkbcomp.returncode else: - self.keymap = stdout + self.keymap = stdout.encode("utf-8") self.exitstatus = 0 if output_dir: @@ -244,8 +264,27 @@ class XkbCompInvocation(Invocation): class XkbcommonInvocation(Invocation): UNRECOGNIZED_KEYSYM_ERROR: ClassVar[str] = "XKB-107" + def _check_stderr(self, stderr: str) -> bool: + if self.UNRECOGNIZED_KEYSYM_ERROR in stderr: + for line in stderr.splitlines(): + if self.UNRECOGNIZED_KEYSYM_ERROR in line: + self.error = line + break + self.exitstatus = 99 # tool doesn't generate this one + return False + else: + self.exitstatus = 0 + return True + @classmethod - def run(cls, i: Self, output_dir: Path | None, compress: int) -> Self: + def run( + cls, + i: Self, + output_dir: Path | None, + compress: int, + *args, + **kwargs, + ) -> Self: i._run(output_dir, compress) return i @@ -265,19 +304,201 @@ class XkbcommonInvocation(Invocation): self.error = "failed to compile keymap" self.exitstatus = err.returncode else: - if self.UNRECOGNIZED_KEYSYM_ERROR in completed.stderr: - for line in completed.stderr.splitlines(): - if self.UNRECOGNIZED_KEYSYM_ERROR in line: - self.error = line - break - self.exitstatus = 99 # tool doesn't generate this one - else: - self.exitstatus = 0 - self.keymap = completed.stdout + if self._check_stderr(completed.stderr): + self.keymap = completed.stdout.encode("utf-8") + if output_dir: + self._write_keymap(output_dir, compress) + + +@dataclass +class CompileKeymapPool: + count: int + pool: tuple[subprocess.Popen] = () + tmp: tempfile.TemporaryDirectory | None = None + socket: Path | None = None + + @property + def sockets(self) -> Iterable[Path]: + if self.socket: + yield self.socket + elif self.tmp: + for k in range(self.count): + yield Path(self.tmp.name) / str(k) + + def __enter__(self): + if not self.pool and not self.socket: + self.tmp = tempfile.TemporaryDirectory() + if self.pool: + for p in self.pool: + p.kill() + pool: list[subprocess.Popen] = [] + for path in self.sockets: + args = ("compile-keymap-server", "--socket", str(path)) + pool.append( + subprocess.Popen( + args, + close_fds=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + # stderr=None, + ) + ) + self.pool = tuple(pool) + return self + + def __exit__(self, exc_type, exc_value, traceback): + if self.socket: + return + for path in self.sockets: + self.bye(path) + sleep(0.5) + if self.pool: + for p in self.pool: + p.kill() + self.pool = () + self.tmp.cleanup() + + @classmethod + def _get_response(cls, s: socket.socket, more: bool) -> tuple[bool, int, bytes]: + # Get size of the message + response = s.recv(ctypes.sizeof(ctypes.c_ssize_t)) + response_size = int.from_bytes(response, byteorder=sys.byteorder, signed=True) + # Get message + response = b"" + if response_size > 0: + to_read = response_size + ba = bytearray(response_size) + view = memoryview(ba) + while to_read: + nbytes = s.recv_into(view, to_read, 0) + view = view[nbytes:] + to_read -= nbytes + response = bytes(ba) + assert len(response) == response_size, (response_size, len(response)) + ok = True + else: + response = b"" + ok = not (response_size < 0) + # Confirm reception + s.sendall(b"1" if more else b"0") + return ok, response_size, response + + @classmethod + def message(cls, socket_path: Path, msg: bytes) -> tuple[bool, bytes, bytes]: + response = b"" + response_size = 0 + stderr = b"" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s: + ok = True + try: + s.settimeout(6) + s.connect(str(socket_path)) + s.sendall(msg) + _ok, response_size, response = cls._get_response(s, True) + ok &= _ok + _ok, response_size, stderr = cls._get_response(s, False) + ok &= _ok + except OSError as err: + print( + "Socket error:", err, ok, response_size, response, file=sys.stderr + ) + traceback.print_exception(err, file=sys.stderr) + ok = False + s.shutdown(socket.SHUT_RDWR) + return ok, response, stderr + + @classmethod + def bye(cls, socket_path: Path) -> None: + try: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s: + s.settimeout(6) + s.connect(str(socket_path)) + s.sendall(b"\x1b") + s.shutdown(socket.SHUT_RDWR) + except OSError: + pass + + @classmethod + def get_keymap( + cls, socket_path: Path, rmlvo: RMLVO, serialize: bool + ) -> tuple[bool, bytes, bytes]: + query = f"{int(serialize)}\n" + "\n".join(v or "" for v in rmlvo) + return cls.message(socket_path, query.encode()) + + +@dataclass +class XkbcommonInvocationServer(XkbcommonInvocation): + def _write(self, fd: BinaryIO) -> None: + super()._write(fd) + if self.keymap: + fd.write(b"\n") + + @classmethod + def run( + cls, + i: Self, + output_dir: Path | None, + compress: int, + socket_path: Path, + *args, + **kwargs, + ) -> Self: + i._run(output_dir, compress, socket_path) + return i + + def _run(self, output_dir: Path | None, compress: int, socket_path: Path) -> None: + serialize = bool(output_dir) + ok, self.keymap, stderr = CompileKeymapPool.get_keymap( + socket_path, self, serialize + ) + + self.exitstatus = 0 if ok else 1 + self._check_stderr(stderr.decode("utf-8")) if output_dir: self._write_keymap(output_dir, compress) + @classmethod + def run_all( + cls, + combos: Iterable[Self], + combos_count: int, + njobs: int, + keymap_output_dir: Path | None, + verbose: bool, + short: bool, + progress_bar: ProgressBar[Iterable[Self]], + chunksize: int, + compress: int, + **kwargs, + ) -> bool: + if keymap_output_dir: + try: + keymap_output_dir.mkdir(parents=True) + except FileExistsError as e: + print(e, file=sys.stderr) + return False + + failed = False + + with CompileKeymapPool(1, socket=kwargs.get("server_socket")) as kmp: + with multiprocessing.Pool(njobs, maxtasksperchild=1000) as p: + f = partial( + cls.run, + socket_path=tuple(kmp.sockets)[0], + output_dir=keymap_output_dir, + compress=compress, + ) + results = p.imap_unordered(f, combos, chunksize=chunksize) + for invocation in progress_bar( + results, total=combos_count, file=sys.stdout + ): + if invocation.exitstatus != 0: + failed = True + invocation._print_result(short, verbose) + + return failed + @dataclass class Layout: @@ -428,6 +649,7 @@ def main() -> NoReturn: ) tools: dict[str, type[Invocation]] = { "libxkbcommon": XkbcommonInvocation, + "libxkbcommon-server": XkbcommonInvocationServer, "xkbcomp": XkbCompInvocation, } parser.add_argument( @@ -463,6 +685,7 @@ def main() -> NoReturn: parser.add_argument( "--layout", default=WILDCARD, type=str, help="Only test the given layout" ) + parser.add_argument("--chunksize", default=1, type=int) parser.add_argument( "--variant", default=WILDCARD, @@ -475,6 +698,7 @@ def main() -> NoReturn: parser.add_argument( "--no-iterations", "-1", action="store_true", help="Only test one combo" ) + parser.add_argument("--socket", type=Path) args = parser.parse_args() @@ -511,7 +735,9 @@ def main() -> NoReturn: verbose, short, progress_bar, - args.compress, + args.chunksize, + server_socket=args.socket, + compress=args.compress, ) sys.exit(failed) diff --git a/tools/compile-keymap-server.c b/tools/compile-keymap-server.c new file mode 100644 index 00000000..8745d461 --- /dev/null +++ b/tools/compile-keymap-server.c @@ -0,0 +1,503 @@ +/* + * Copyright © 2024 Pierre Le Marre + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "xkbcommon/xkbcommon.h" +#include "src/context.h" +#include "tools-common.h" +#include "src/utils.h" + +#define DEFAULT_INCLUDE_PATH_PLACEHOLDER "__defaults__" + +static bool verbose = false; +static const char *includes[64]; +static size_t num_includes = 0; + +static void +usage(char **argv) +{ + printf("Usage: %s [OPTIONS]\n" + "\n" + "Start a server to compile keymaps\n" + "Options:\n" + " --help\n" + " Print this help and exit\n" + " --verbose\n" + " Enable verbose debugging output\n" + " --socket \n" + " Path of the Unix socket\n" + " --include\n" + " Add the given path to the include path list. This option is\n" + " order-dependent, include paths given first are searched first.\n" + " If an include path is given, the default include path list is\n" + " not used. Use --include-defaults to add the default include\n" + " paths\n" + " --include-defaults\n" + " Add the default set of include directories.\n" + " This option is order-dependent, include paths given first\n" + " are searched first.\n" + "\n", + argv[0]); +} + +static bool +parse_options(int argc, char **argv, struct xkb_rule_names *names, + char **socket_address) +{ + enum options { + OPT_VERBOSE, + OPT_INCLUDE, + OPT_INCLUDE_DEFAULTS, + OPT_SOCKET, + }; + static struct option opts[] = { + {"help", no_argument, 0, 'h'}, + {"verbose", no_argument, 0, OPT_VERBOSE}, + {"include", required_argument, 0, OPT_INCLUDE}, + {"include-defaults", no_argument, 0, OPT_INCLUDE_DEFAULTS}, + {"socket", required_argument, 0, OPT_SOCKET}, + {0, 0, 0, 0}, + }; + + while (1) { + int c; + int option_index = 0; + c = getopt_long(argc, argv, "h", opts, &option_index); + if (c == -1) + break; + + switch (c) { + case 'h': + usage(argv); + exit(0); + case OPT_VERBOSE: + verbose = true; + break; + case OPT_SOCKET: + *socket_address = optarg; + break; + case OPT_INCLUDE: + if (num_includes >= ARRAY_SIZE(includes)) { + fprintf(stderr, "error: too many includes\n"); + exit(EXIT_INVALID_USAGE); + } + includes[num_includes++] = optarg; + break; + case OPT_INCLUDE_DEFAULTS: + if (num_includes >= ARRAY_SIZE(includes)) { + fprintf(stderr, "error: too many includes\n"); + exit(EXIT_INVALID_USAGE); + } + includes[num_includes++] = DEFAULT_INCLUDE_PATH_PLACEHOLDER; + break; + default: + usage(argv); + exit(EXIT_INVALID_USAGE); + } + } + + return true; +} + +#define INPUT_BUFFER_SIZE 1024 + +static pthread_mutex_t server_state_mutext = PTHREAD_MUTEX_INITIALIZER; +static volatile int socket_fd; +static volatile bool serving = false; + +static void +shutdown_server(void) +{ + pthread_mutex_lock(&server_state_mutext); + fprintf(stderr, "Shuting down. Bye!\n"); + serving = false; + shutdown(socket_fd, SHUT_RD); + pthread_mutex_unlock(&server_state_mutext); +} + +static void +handle_signal(int signum) +{ + switch (signum) { + case SIGINT: + shutdown_server(); + break; + } +} + +/* Load parser for a RMLVO component */ +static bool +parse_component(char **input, size_t *max_len, const char **output) +{ + if (!(*input) || !(*max_len)) { + *input = NULL; + *max_len = 0; + *output = NULL; + return true; + } + char *start = *input; + char *next = strchr(start, '\n'); + size_t len; + if (next == NULL) { + len = *max_len; + *input = NULL; + *max_len = 0; + } else { + len = (size_t)(next - start); + *max_len -= len + 1; + *input += len + 1; + } + *output = strndup(start, len); + if (!(*output)) { + fprintf(stderr, "ERROR: Cannot allocate memory\n"); + return false; + } + return true; +} + +struct query_args { + struct xkb_context *ctx; + int accept_socket_fd; +}; + +static const char * +log_level_to_prefix(enum xkb_log_level level) +{ + switch (level) { + case XKB_LOG_LEVEL_DEBUG: + return "xkbcommon: DEBUG: "; + case XKB_LOG_LEVEL_INFO: + return "xkbcommon: INFO: "; + case XKB_LOG_LEVEL_WARNING: + return "xkbcommon: WARNING: "; + case XKB_LOG_LEVEL_ERROR: + return "xkbcommon: ERROR: "; + case XKB_LOG_LEVEL_CRITICAL: + return "xkbcommon: CRITICAL: "; + default: + return NULL; + } +} + +ATTR_PRINTF(3, 0) static void +keymap_log_fn(struct xkb_context *ctx, enum xkb_log_level level, + const char *fmt, va_list args) +{ + const char *prefix = log_level_to_prefix(level); + FILE *file = xkb_context_get_user_data(ctx); + + if (prefix) + fprintf(file, "%s", prefix); + vfprintf(file, fmt, args); +} + +/* Process client’s queries */ +static void* +process_query(void *x) +{ + struct query_args *args = x; + int rc = EXIT_FAILURE; + char input_buffer[INPUT_BUFFER_SIZE]; + + /* Loop while client send queries */ + ssize_t count; + while ((count = recv(args->accept_socket_fd, + input_buffer, INPUT_BUFFER_SIZE, 0)) > 0) { + rc = EXIT_FAILURE; + bool stop = true; + if (input_buffer[0] == '\x1b') { + /* Escape = exit */ + rc = -0x1b; + break; + } + + /* + * Expected message load: + * <1|0>\n\n\n\nvariant>\n + */ + + if (count < 3 || input_buffer[1] != '\n') { + /* Invalid length */ + break; + } + bool serialize = input_buffer[0] == '1'; + + /* We expect RMLVO to be provided with one component per line */ + char *input = input_buffer + 2; + size_t len = count - 2; + struct xkb_rule_names rmlvo = { + .rules = NULL, + .model = NULL, + .layout = NULL, + .variant = NULL, + .options = NULL, + }; + if (!parse_component(&input, &len, &rmlvo.rules) || + !parse_component(&input, &len, &rmlvo.model) || + !parse_component(&input, &len, &rmlvo.layout) || + !parse_component(&input, &len, &rmlvo.variant) || + !parse_component(&input, &len, &rmlvo.options)) { + fprintf(stderr, "ERROR: Cannor parse RMLVO: %s.\n", input_buffer); + goto error; + } + + /* Response load: */ + + /* Compile keymap */ + struct xkb_keymap *keymap; + /* Clone context because it is not thread-safe */ + struct xkb_context ctx = *args->ctx; + + /* Set our own logging function to capture default stderr */ + char *stderr_buffer = NULL; + size_t stderr_size = 0; + FILE *stderr_new = open_memstream(&stderr_buffer, &stderr_size); + if (!stderr_new) { + perror("Failed to create in-memory stderr."); + goto stderr_error; + } + xkb_context_set_user_data(&ctx, stderr_new); + xkb_context_set_log_fn(&ctx, keymap_log_fn); + + rc = EXIT_SUCCESS; + + keymap = xkb_keymap_new_from_names(&ctx, &rmlvo, XKB_KEYMAP_COMPILE_NO_FLAGS); + if (keymap == NULL) { + /* Send negative message length to convey error */ + count = -1; + send(args->accept_socket_fd, &count, sizeof(count), MSG_NOSIGNAL); + goto keymap_error; + } + + /* Send the keymap, if required */ + if (serialize) { + char *buf = xkb_keymap_get_as_string(keymap, XKB_KEYMAP_FORMAT_TEXT_V1); + if (buf) { + len = strlen(buf); + /* Send message length */ + send(args->accept_socket_fd, &len, sizeof(len), MSG_NOSIGNAL); + send(args->accept_socket_fd, buf, len, MSG_NOSIGNAL); + free(buf); + } else { + count = -1; + send(args->accept_socket_fd, &count, sizeof(count), MSG_NOSIGNAL); + } + } else { + len = 0; + send(args->accept_socket_fd, &len, sizeof(len), MSG_NOSIGNAL); + } + + xkb_keymap_unref(keymap); + +keymap_error: + /* Wait that the client confirm the reception */ + recv(args->accept_socket_fd, input_buffer, 1, 0); + + /* Restore stderr for logging */ + fflush(stderr_new); + xkb_context_set_user_data(&ctx, stderr); + fclose(stderr_new); + + /* Send captured stderr */ + send(args->accept_socket_fd, &stderr_size, sizeof(stderr_size), MSG_NOSIGNAL); + if (stderr_size && stderr_buffer) { + send(args->accept_socket_fd, stderr_buffer, stderr_size, MSG_NOSIGNAL); + } + free(stderr_buffer); + + /* Wait that the client confirm the reception */ + input_buffer[0] = '0'; + recv(args->accept_socket_fd, input_buffer, 1, 0); + stop = input_buffer[0] == '0'; + +stderr_error: +error: + free((char*)rmlvo.rules); + free((char*)rmlvo.model); + free((char*)rmlvo.layout); + free((char*)rmlvo.variant); + free((char*)rmlvo.options); + + /* Close client connection if there was an error */ + if (rc != EXIT_SUCCESS || stop) + break; + } + xkb_context_unref(args->ctx); + close(args->accept_socket_fd); + free(args); + if (rc > 0) { + fprintf(stderr, "ERROR: failed to process query. Code: %d\n", rc); + } else if (rc < 0) { + shutdown_server(); + } + return NULL; +} + +/* Create a server using Unix sockets */ +static int +serve(struct xkb_context *ctx, const char* socket_address) +{ + int rc = EXIT_FAILURE; + struct sockaddr_un sockaddr_un = { 0 }; + socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_fd == -1) { + fprintf(stderr, "ERROR: Cannot create Unix socket.\n"); + return EXIT_FAILURE; + } + /* Construct the bind address structure. */ + sockaddr_un.sun_family = AF_UNIX; + strcpy(sockaddr_un.sun_path, socket_address); + + rc = bind(socket_fd, (struct sockaddr*) &sockaddr_un, + sizeof(struct sockaddr_un)); + /* If socket_address exists on the filesystem, then bind will fail. */ + if (rc == -1) { + fprintf(stderr, "ERROR: Cannot create Unix socket path.\n"); + rc = EXIT_FAILURE; + goto error_bind; + }; + if (listen(socket_fd, 4096) == -1) { + fprintf(stderr, "ERROR: Cannot listen socket.\n"); + goto error; + } + signal(SIGINT, handle_signal); + fprintf(stderr, "Serving...\n"); + serving = true; + + struct timeval timeout = { + .tv_sec = 3, + .tv_usec = 0 + }; + + while (1) { + int accept_socket_fd = accept(socket_fd, NULL, NULL); + if (accept_socket_fd == -1) { + if (serving) { + fprintf(stderr, "ERROR: fail to accept query\n"); + rc = EXIT_FAILURE; + } else { + rc = EXIT_SUCCESS; + } + goto error; + }; + + if (accept_socket_fd > 0) { + /* Client is connected */ + pthread_t thread; + + /* Prepare worker’s context */ + struct query_args *args = calloc(1, sizeof(struct query_args)); + if (!args) { + close(accept_socket_fd); + continue; + } + /* Context will be cloned in worker */ + args->ctx = xkb_context_ref(ctx); + args->accept_socket_fd = accept_socket_fd; + + if (setsockopt(accept_socket_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, + sizeof timeout) < 0) + perror("setsockopt failed\n"); + + /* Launch worker */ + rc = pthread_create(&thread, NULL, process_query, args); + if (rc) { + perror("Error creating thread: "); + close(accept_socket_fd); + free(args); + continue; + } + pthread_detach(thread); + } + } +error: + close(socket_fd); + unlink(socket_address); +error_bind: + fprintf(stderr, "Exiting...\n"); + return rc; +} + +int +main(int argc, char **argv) +{ + struct xkb_context *ctx; + struct xkb_rule_names names = { + .rules = DEFAULT_XKB_RULES, + .model = DEFAULT_XKB_MODEL, + /* layout and variant are tied together, so we either get user-supplied for + * both or default for both, see below */ + .layout = NULL, + .variant = NULL, + .options = DEFAULT_XKB_OPTIONS, + }; + char *socket_address = NULL; + int rc = EXIT_FAILURE; + + if (argc < 1) { + usage(argv); + return EXIT_INVALID_USAGE; + } + + if (!parse_options(argc, argv, &names, &socket_address)) + return EXIT_INVALID_USAGE; + + ctx = xkb_context_new(XKB_CONTEXT_NO_DEFAULT_INCLUDES); + assert(ctx); + + if (verbose) { + xkb_context_set_log_level(ctx, XKB_LOG_LEVEL_DEBUG); + xkb_context_set_log_verbosity(ctx, 10); + } + + if (num_includes == 0) + includes[num_includes++] = DEFAULT_INCLUDE_PATH_PLACEHOLDER; + + for (size_t i = 0; i < num_includes; i++) { + const char *include = includes[i]; + if (strcmp(include, DEFAULT_INCLUDE_PATH_PLACEHOLDER) == 0) + xkb_context_include_path_append_default(ctx); + else + xkb_context_include_path_append(ctx, include); + } + + serve(ctx, socket_address); + + xkb_context_unref(ctx); + + return rc; +}