19 Commits
Author SHA1 Message Date
sami e0f22f808f merge: lane/pkg-qa
CI / clang-format (push) Waiting to run
CI / testserver (push) Waiting to run
CI / bootstrap-script (push) Waiting to run
CI / bootstrap-script-2604 (push) Waiting to run
CI / extension-lint (push) Waiting to run
CI / build (clang) (push) Waiting to run
CI / build (gcc) (push) Waiting to run
CI / sanitizers (dev) (push) Waiting to run
CI / sanitizers (tsan) (push) Waiting to run
CI / clang-tidy (push) Waiting to run
CI / conformance (push) Waiting to run
CI / nightly-integration (push) Waiting to run
CI / gui-dod (push) Waiting to run
CI / gui-dod-nightly (push) Waiting to run
2026-09-15 18:46:34 +04:00
sami 111229686c merge: lane/daemon 2026-09-15 18:46:34 +04:00
sami a5551db1c3 merge: lane/core 2026-09-15 18:46:34 +04:00
samiandClaude Sonnet 5 83b92d118b core: select platform/linux sources via velox_platform_sources()
Replace the hardcoded src/io/platform/linux/file_ops.cpp entry with a
velox_platform_sources(veloxcore src/io) call, now that PKG/QA's
cmake/platform.cmake owns VELOX_OS_* detection and source selection
(ADR 0020 §5). Adding platform/macos/ later needs no edit to this file.

Build-wiring only; full suite stays green (59/59).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-15 18:46:13 +04:00
samiandClaude Sonnet 5 78f2b22c8c daemon: use velox_platform_sources() instead of a hardcoded platform/linux/ list
PKG's cmake/platform.cmake (VELOX_OS_* + velox_platform_sources(<target> <dir>))
is on main now. Swap the hardcoded src/rpc/platform/linux/*.cpp list in
veloxd_rpc for velox_platform_sources(veloxd_rpc src/rpc), matching ADR 0020 §5
(source selection lives in cmake/platform.cmake and nowhere else). PORT can now
add platform/macos/ with no edit to this file.

Build-wiring only, no behaviour change. Full suite 59/59 green.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-15 18:46:07 +04:00
samiandClaude Sonnet 5 77bdb0c427 docs: mark GUI's bootstrap.sh apt-name-validation request (R2) resolved
tools/bootstrap.sh already validates every apt package name via apt-cache policy
(landed in an earlier session), so R2 was done but still read as an open request.
Mark it closed so it stops resurfacing.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0176u3fTrxegrGNm2yC7r69W
2026-09-15 18:39:03 +04:00
sami d60cf94905 merge: lane/pkg-qa — local .deb, cmake/platform.cmake, Linux-gated deps
CI / clang-format (push) Canceled after 0s
CI / testserver (push) Canceled after 0s
CI / bootstrap-script (push) Canceled after 0s
CI / bootstrap-script-2604 (push) Canceled after 0s
CI / extension-lint (push) Canceled after 0s
CI / build (clang) (push) Canceled after 0s
CI / build (gcc) (push) Canceled after 0s
CI / sanitizers (dev) (push) Canceled after 0s
CI / sanitizers (tsan) (push) Canceled after 0s
CI / clang-tidy (push) Canceled after 0s
CI / conformance (push) Canceled after 0s
CI / nightly-integration (push) Canceled after 0s
CI / gui-dod (push) Canceled after 0s
CI / gui-dod-nightly (push) Canceled after 0s
2026-09-15 18:35:11 +04:00
samiandClaude Sonnet 5 0cd5bf6882 pkg: cmake/platform.cmake; gate libsecret and Qt6::DBus to Linux
Root build infra, per ADR 0020 as amended in 9624e68: PORT owns the per-OS
backend directories and macOS/Windows packaging, but cmake/platform*.cmake stays
PKG/QA's — the amendment exists because Phase 0 (CORE's and DAEMON's own seams,
on Linux) needs velox_platform_sources() to select platform/linux/*.cpp, and PORT
can't start until Phase 0 lands. Landing it as PORT-owned would have deadlocked
both lanes waiting on each other.

cmake/platform.cmake sets exactly one of VELOX_OS_LINUX/VELOX_OS_MACOS/
VELOX_OS_WINDOWS from CMAKE_SYSTEM_NAME and exposes velox_platform_sources(target
dir), which globs dir/platform/<os>/*.cpp. No seam consumes it yet (Phase 0 hasn't
landed), so on Ubuntu this changes nothing observable — VELOX_OS_LINUX is simply
true and no lane calls the new function.

pkg_check_modules(LIBSECRET ...) was unconditional in the root CMakeLists.txt,
so macOS could not configure at all (docs/08-porting.md's stated blocker); it's
now gated on VELOX_OS_LINUX, same as the Qt6::DBus component this also adds
(gui/docs/pkg-qa-requests-m1.md R4 — backs the org.freedesktop.portal
GlobalShortcuts path, which is Linux/portal-only same as libsecret). Both are
still REQUIRED on Linux; PORT adds the macOS sides (Keychain, and no DBus
equivalent needed) behind their own seams, not by touching this find.

Verified: fresh configure + build of veloxd/velox/velox-gui/velox-nmhost still
green (libsecret-1 and Qt6::DBus both found), the .deb from the previous commit
still builds and installs.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0176u3fTrxegrGNm2yC7r69W
2026-09-15 18:03:50 +04:00
samiandClaude Sonnet 5 31eadffce5 pkg: build a local .deb; de-duplicate onto DAEMON's systemd/nativehost/man page
DAEMON landed packaging/nativehost/, packaging/systemd/{velox.service,velox.socket}
(real sd_listen_fds() socket activation) and cli/man/velox.1 on main while this was
in flight, duplicating paths this branch had drafted independently. Per CLAUDE.md
(packaging/nativehost/ is DAEMON's; the rest of packaging/ is PKG/QA's), DAEMON's
copies are kept as the single source and this branch's packaging/native-messaging/,
packaging/man/velox.1 and stale packaging/systemd/velox.service are dropped rather
than maintained twice. The root CMakeLists.txt's install() rules now point at
DAEMON's paths directly, install both systemd units (not just velox.service), and
docs/07-packaging.md / packaging/README.md no longer claim there's no socket
activation — there is, and this package now ships it.

Adds the local-test-only .deb itself: debian/{control,rules,postinst,postrm,
changelog,copyright}, a placeholder icon set, the desktop entry and PKG/QA's own
man pages (veloxd.8, velox-gui.1 — velox.1 stays DAEMON's). libveloxcore stays
static (no consumer needs a .so yet); pairing has no real approval UI in this
build (D1 unbuilt), so postinst and packaging/README.md both say VELOX_PAIR_AUTO=1
is required and print it prominently. No PPA, no GPG — local dpkg -i only.

Verified live: dpkg-buildpackage -us -uc -b, dpkg -i, socket activation (systemctl
--user enable --now velox.socket), `velox ls` against the running daemon, lintian
clean, dpkg -r removes the manifest/units and leaves $XDG_DATA_HOME/velox alone.

.gitignore now excludes dpkg-buildpackage's debhelper build tree and obj-*/ (the
debuild output dir) — debian/control et al. stay tracked, everything debhelper
regenerates does not.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0176u3fTrxegrGNm2yC7r69W
2026-09-15 18:02:11 +04:00
sami 5b42e1f0e5 merge: lane/gui
CI / clang-format (push) Canceled after 0s
CI / testserver (push) Canceled after 0s
CI / bootstrap-script (push) Canceled after 0s
CI / bootstrap-script-2604 (push) Canceled after 0s
CI / extension-lint (push) Canceled after 0s
CI / build (clang) (push) Canceled after 0s
CI / build (gcc) (push) Canceled after 0s
CI / sanitizers (dev) (push) Canceled after 0s
CI / sanitizers (tsan) (push) Canceled after 0s
CI / clang-tidy (push) Canceled after 0s
CI / conformance (push) Canceled after 0s
CI / nightly-integration (push) Canceled after 0s
CI / gui-dod (push) Canceled after 0s
CI / gui-dod-nightly (push) Canceled after 0s
2026-09-15 17:57:36 +04:00
sami e8af9b46b1 merge: lane/daemon 2026-09-15 17:57:36 +04:00
sami 8abfc6a428 merge: lane/core 2026-09-15 17:57:36 +04:00
samiandClaude Sonnet 5 936136db06 core: introduce the file_ops platform seam (ADR 0020 Phase 0)
Pure refactor, zero behaviour change. posix_fallocate/ftruncate,
posix_fadvise(DONTNEED), and fdatasync move out of sparse_file.cpp and
meta/veloxpart.cpp into core/src/io/platform/linux/file_ops.cpp behind
a new seam (core/src/io/platform/file_ops.hpp). Neither caller names a
Linux-only syscall anymore, and no #ifdef exists outside platform/linux/.
Clears the way for the PORT lane's macOS backend.

Full suite: 44/44 green (build/dev).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-15 17:56:02 +04:00
samiandClaude Sonnet 5 6d7c4f7bc4 daemon: Phase 0 platform seams (ADR 0020) — eventfd/SO_PEERCRED/instance-lock/XDG behind platform/, zero behaviour change
Introduces the four rpc-layer seams docs/08-porting.md calls for this lane:
platform::Wakeup (eventfd), platform::peer_of (SO_PEERCRED/struct ucred, same-UID
check preserved unchanged), platform::acquire_instance_lock (abstract-namespace
socket), platform::runtime_base_dir/data_base_dir (XDG lookups). Today's Linux
code moves unchanged into daemon/src/rpc/platform/linux/; the seam headers
carry no OS types and no #ifdef.

No fifth interface for timerfd: EventLoop gains a portable add_timer() that
folds the next deadline into poll()'s own timeout, replacing both timerfd
instances in main.cpp — the loop already computes a deadline, so this needs no
per-OS backend at all.

Full suite 59/59 green; no #ifdef outside platform/linux/, no behaviour change.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-15 17:54:32 +04:00
samiandClaude Sonnet 5 9487202ea8 gui: ship the real icon set, wire toolbar/tray/window icon, rebase onto main
Adwaita-derived (GNOME Project, LGPL-3-or-CC-BY-SA), recoloured per light/dark
skin and laid out in IDM's toolbar positions per docs/03-gui-spec.md §1.
Licence and per-file provenance recorded in gui/resources/icons/LICENSE.

New IconTheme mirrors ThemeManager's colorSchemeChanged hook so a live
light/dark switch swaps every action's glyph, not just the QSS. Toolbar/menu
actions, the tray icon and the window icon all now use it instead of the
SP_ArrowDown placeholder. Install-path notes for PKG filed as R5 in
gui/docs/pkg-qa-requests-m1.md.

Also rebases lane/gui onto main (13 commits, no gui/ conflicts).

Verified: full build clean, all 13 gui ctest targets pass (including
gui_no_download_logic), unhappy-path DoD gate PASS against mockd
(--drop-connection).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-15 17:50:51 +04:00
samiandClaude Opus 5 9624e685af docs: cmake/platform.cmake belongs to PKG/QA, not PORT (ADR 0020 amendment)
CI / clang-format (push) Canceled after 0s
CI / testserver (push) Canceled after 0s
CI / bootstrap-script (push) Canceled after 0s
CI / bootstrap-script-2604 (push) Canceled after 0s
CI / extension-lint (push) Canceled after 0s
CI / build (clang) (push) Canceled after 0s
CI / build (gcc) (push) Canceled after 0s
CI / sanitizers (dev) (push) Canceled after 0s
CI / sanitizers (tsan) (push) Canceled after 0s
CI / clang-tidy (push) Canceled after 0s
CI / conformance (push) Canceled after 0s
CI / nightly-integration (push) Canceled after 0s
CI / gui-dod (push) Canceled after 0s
CI / gui-dod-nightly (push) Canceled after 0s
As written, ADR 0020 gave PORT ownership of cmake/platform*.cmake while also
making PORT wait for Phase 0 — but Phase 0's seams are exactly what needs
velox_platform_sources() to select platform/linux/*.cpp. PORT cannot start until
Phase 0 lands, and Phase 0 cannot land without the file PORT owns. PKG/QA caught
the circularity before writing anything, which was the right call.

Central OS detection is root build infrastructure, so it stays with PKG/QA and
lands during Phase 0. PORT keeps the per-OS backend directories and the macOS
and Windows packaging, and owns no part of cmake/.

Also drops a hardcoded test count from the PORT definition of done; the suite
number moves every round and the gate is that it stays green.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-15 17:46:48 +04:00
sami 4e87730ce9 merge: lane/daemon — velox-nmhost, systemd units, velox(1) man page
CI / clang-format (push) Canceled after 0s
CI / testserver (push) Canceled after 0s
CI / bootstrap-script (push) Canceled after 0s
CI / bootstrap-script-2604 (push) Canceled after 0s
CI / extension-lint (push) Canceled after 0s
CI / build (clang) (push) Canceled after 0s
CI / build (gcc) (push) Canceled after 0s
CI / sanitizers (dev) (push) Canceled after 0s
CI / sanitizers (tsan) (push) Canceled after 0s
CI / clang-tidy (push) Canceled after 0s
CI / conformance (push) Canceled after 0s
CI / nightly-integration (push) Canceled after 0s
CI / gui-dod (push) Canceled after 0s
CI / gui-dod-nightly (push) Canceled after 0s
2026-09-15 17:38:46 +04:00
samiandClaude Opus 5 facd851824 docs: define the cross-platform porting strategy (ADR 0020, macOS first)
CI / clang-format (push) Canceled after 0s
CI / testserver (push) Canceled after 0s
CI / bootstrap-script (push) Canceled after 0s
CI / bootstrap-script-2604 (push) Canceled after 0s
CI / extension-lint (push) Canceled after 0s
CI / build (clang) (push) Canceled after 0s
CI / build (gcc) (push) Canceled after 0s
CI / sanitizers (dev) (push) Canceled after 0s
CI / sanitizers (tsan) (push) Canceled after 0s
CI / clang-tidy (push) Canceled after 0s
CI / conformance (push) Canceled after 0s
CI / nightly-integration (push) Canceled after 0s
CI / gui-dod (push) Canceled after 0s
CI / gui-dod-nightly (push) Canceled after 0s
The tree is Ubuntu-first, but the Linux-specific surface is about ten files and
already sits at the bottom of the I/O and RPC stacks. Rather than fork the repo
or scatter ifdefs, platform differences go behind narrow seams with one backend
directory per OS, selected centrally in cmake/platform.cmake.

Linux stays the reference implementation and its behaviour does not change: the
owning lanes (CORE, DAEMON) move today's Linux code behind the seams on Ubuntu
first, with the suite still green, before any macOS code exists. A new PORT lane
owns only the per-OS backends, so macOS work never writes shared logic and
CLAUDE.md's lane rule stays intact.

Adds ADR 0020 (the decision and phases), docs/08-porting.md (seam list, API
mapping for macOS and Windows, dependency table, verification gates) and
docs/agents/AGENT-PORT.md (the lane brief), plus the PORT row in the ownership
table.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-15 17:36:27 +04:00
samiandClaude Sonnet 5 eb72aa522c daemon: build velox-nmhost, systemd socket activation + units, velox(1) man page
Closes build order items 7 (the systemd half) and 9 (D11 in deferrals.md).

velox-nmhost (nmhost/src/main.cpp, 185 lines): a poll()-driven byte pump between
Firefox's native-messaging framing on stdio (4-byte native-byte-order length prefix)
and veloxd's own NDJSON framing on the Unix socket. Reframes each direction, no JSON
parsing, no retry/backoff (the extension relaunches a fresh host on its own
reconnect), exits the moment either side closes. Deliberately dependency-free — no
veloxd_* library, no nlohmann_json — since it runs unconfined outside Firefox's
sandbox regardless of packaging format.

Two real bugs found and fixed while getting the integration test to actually pass
rather than hang, both exactly the class of bug a "trivial pump" invites:
1. Never set the pumped fds non-blocking, so the "drain what's available" read loop
   blocked on its own second read() instead of returning to poll().
2. stdin and stdout are two different descriptors (0 and 1), not one — an early draft
   polled POLLOUT on fd 0, which is opened read-only, so EOF and writability were
   never both observable through the same pollfd entry.

packaging/nativehost/com.velox.host.json + its own README.md supersede
AGENT-DAEMON.md's stale "four locations" line: spike S1 / ADR 0003 found only three
manifest locations are real (~/.mozilla/native-messaging-hosts/ for BOTH deb/tarball
and snap Firefox, /usr/lib/mozilla/... for deb/tarball only, the flatpak sandbox path)
— the fourth, ~/snap/firefox/common/.mozilla/..., is not read by snap Firefox at all.
The README spells out the per-user-manifest / postinst enumeration implication for
PKG/QA (postinst runs once as root; the two ~/-relative locations are per-user) and
flags that docs/07-packaging.md's own install-layout line only shows the one
root-owned path.

Socket activation: rpc/systemd_activation.cpp is a from-scratch sd_listen_fds() (env
vars only — LISTEN_PID/LISTEN_FDS, fd 3 — no libsystemd link) that UdsServer::start()
checks first, skipping its own create/bind/chmod/listen when systemd already bound
the socket. packaging/systemd/velox.socket + velox.service are the unit pair,
verified both by systemd-analyze verify and by an actual fork/dup2/execve simulation
of the activation handshake — a real session.hello round-tripped over the handed-off
fd with no bind() ever called inside the daemon for that run. velox.service
deliberately skips ProtectSystem=/ProtectHome=/ReadWritePaths=: saveTo.allowedRoots is
user-configurable to anywhere on the filesystem, and a sandbox here would turn a
legitimately-configured save location into an opaque EROFS/EACCES instead of the
daemon's own clear -32011.

cli/man/velox.1 documents the CLI as it actually exists today (add/ls/pause/resume/rm,
--json) — the queue/settings subcommands AGENT-DAEMON.md's build order originally
sketched aren't implemented in cli/src/main.cpp yet, so the page doesn't claim they
are. Checked warning-free with groff -mandoc -ww -z.

Full ctest: 57/57 (excluding the pre-existing, unrelated conformance failure noted in
earlier commits).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
2026-09-12 22:46:52 +04:00
101 changed files with 2471 additions and 184 deletions
+14
View File
@@ -45,3 +45,17 @@ crash-*
oom-* oom-*
leak-* leak-*
timeout-* timeout-*
# dpkg-buildpackage output — debhelper's build tree and the produced .deb/.changes, plus
# the classic out-of-tree autotools-style build dir some debhelper versions still create.
# debian/control, changelog, copyright, rules, postinst, postrm, source/format and
# *.lintian-overrides are real packaging source and stay tracked; everything below is
# regenerated by `dpkg-buildpackage` on every run.
debian/.debhelper/
debian/velox/
debian/files
debian/*.substvars
debian/*.debhelper.log
debian/*.debhelper
debian/debhelper-build-stamp
obj-*/
+1
View File
@@ -12,6 +12,7 @@ Read this before touching anything. Then read your lane brief in [docs/agents/](
| GUI | `gui/` | everything else | | GUI | `gui/` | everything else |
| EXT | `extension/` | everything else | | EXT | `extension/` | everything else |
| PKG/QA | `packaging/`, `.github/`, `tools/testserver/`, `tests/integration/`, `tests/e2e/`, root build files | any lane's feature code | | PKG/QA | `packaging/`, `.github/`, `tools/testserver/`, `tests/integration/`, `tests/e2e/`, root build files | any lane's feature code |
| PORT | `**/platform/<os>/**`, `packaging/macos/`, `packaging/windows/` | all logic, and `cmake/` — see [ADR 0020](docs/adr/0020-cross-platform-strategy.md) |
If your task seems to require editing another lane's files, that is a signal the interface If your task seems to require editing another lane's files, that is a signal the interface
is wrong. **File the request; don't reach across.** is wrong. **File the request; don't reach across.**
+76
View File
@@ -29,6 +29,10 @@ option(VELOX_BUILD_FUZZ "Build libFuzzer targets (clang only)" OFF)
option(VELOX_ENABLE_MEDIA "Build the HLS/DASH media grabber (M4)" OFF) option(VELOX_ENABLE_MEDIA "Build the HLS/DASH media grabber (M4)" OFF)
option(VELOX_WERROR "Treat warnings as errors" ON) option(VELOX_WERROR "Treat warnings as errors" ON)
# Central OS detection (docs/adr/0020-cross-platform-strategy.md). Included early: the
# dependency finds below gate Linux-only libraries on VELOX_OS_LINUX.
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/platform.cmake)
# Project-wide warning flags. Set via add_compile_options (a directory property), not the # Project-wide warning flags. Set via add_compile_options (a directory property), not the
# CMAKE_CXX_FLAGS cache variable, because the dev/tsan presets overwrite that cache var # CMAKE_CXX_FLAGS cache variable, because the dev/tsan presets overwrite that cache var
# wholesale for sanitizer flags — a target's warnings must not ride on it. # wholesale for sanitizer flags — a target's warnings must not ride on it.
@@ -55,12 +59,24 @@ endif()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/daemon/CMakeLists.txt) if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/daemon/CMakeLists.txt)
find_package(SQLite3 REQUIRED) find_package(SQLite3 REQUIRED)
find_package(nlohmann_json 3.11 REQUIRED) find_package(nlohmann_json 3.11 REQUIRED)
# libsecret / Secret Service is Linux-only (docs/adr/0020-cross-platform-strategy.md,
# docs/08-porting.md): macOS uses Keychain behind the same credential-store seam, so
# this stays REQUIRED on Linux and simply absent elsewhere — PORT's job is to add the
# macOS side of that seam, not to touch this find.
if(VELOX_OS_LINUX)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBSECRET REQUIRED IMPORTED_TARGET libsecret-1) pkg_check_modules(LIBSECRET REQUIRED IMPORTED_TARGET libsecret-1)
endif()
endif() endif()
if(VELOX_BUILD_GUI AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/gui/CMakeLists.txt) if(VELOX_BUILD_GUI AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/gui/CMakeLists.txt)
# DBus (gui/docs/pkg-qa-requests-m1.md R4) backs the org.freedesktop.portal
# GlobalShortcuts path — Linux/portal-only, same reasoning as libsecret above.
if(VELOX_OS_LINUX)
find_package(Qt6 6.6 REQUIRED COMPONENTS Widgets Svg Network DBus LinguistTools)
else()
find_package(Qt6 6.6 REQUIRED COMPONENTS Widgets Svg Network LinguistTools) find_package(Qt6 6.6 REQUIRED COMPONENTS Widgets Svg Network LinguistTools)
endif()
qt_standard_project_setup() qt_standard_project_setup()
endif() endif()
@@ -105,3 +121,63 @@ if(VELOX_BUILD_TESTS)
endif() endif()
endforeach() endforeach()
endif() endif()
# --- Install rules (M6 packaging; docs/07-packaging.md; owned by PKG/QA) ------------
#
# Guarded by `if(TARGET ...)`, not by editing each lane's own CMakeLists.txt: install()
# is a packaging concern, and reaching into daemon/CMakeLists.txt (DAEMON's), cli's build
# file (also DAEMON's) or gui/CMakeLists.txt (GUI's) to add it there would cross a lane
# boundary CLAUDE.md draws on purpose. Every target below is defined by its own lane;
# this only says where the packaged binary already built by that lane's rules goes.
#
# libveloxcore is intentionally absent here: it stays a static library linked into each
# binary (user decision, see docs/07-packaging.md) — there is no .so to install.
#
# CMAKE_INSTALL_LIBDIR is multiarch-adjusted (lib/x86_64-linux-gnu/) by GNUInstallDirs on
# Debian; systemd user units and the Mozilla native-messaging directory are NOT
# architecture-specific paths, so those two destinations are written literally
# (lib/systemd/user, lib/mozilla/...) rather than built from that variable.
include(GNUInstallDirs)
if(TARGET veloxd)
install(TARGETS veloxd RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES ${CMAKE_SOURCE_DIR}/packaging/man/veloxd.8
DESTINATION ${CMAKE_INSTALL_MANDIR}/man8)
# velox.socket ships alongside velox.service: DAEMON wired real sd_listen_fds()
# socket activation (daemon/src/rpc/systemd_activation.cpp) — see
# packaging/systemd/README.md for the pair's own rationale.
install(FILES ${CMAKE_SOURCE_DIR}/packaging/systemd/velox.service
${CMAKE_SOURCE_DIR}/packaging/systemd/velox.socket
DESTINATION lib/systemd/user)
endif()
if(TARGET velox)
install(TARGETS velox RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
# DAEMON owns cli/ and its man page (CLAUDE.md); packaging only installs it.
install(FILES ${CMAKE_SOURCE_DIR}/cli/man/velox.1
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
endif()
if(TARGET velox-gui)
install(TARGETS velox-gui RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES ${CMAKE_SOURCE_DIR}/packaging/man/velox-gui.1
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES ${CMAKE_SOURCE_DIR}/packaging/desktop/velox.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
foreach(iconsize 16 22 24 32 48 64 128 256)
install(FILES
${CMAKE_SOURCE_DIR}/packaging/icons/hicolor/${iconsize}x${iconsize}/apps/velox.png
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${iconsize}x${iconsize}/apps)
endforeach()
endif()
# nmhost and its manifest are DAEMON's (CLAUDE.md: nmhost/ and packaging/nativehost/).
# This lights up the moment DAEMON's target exists, the same way the add_subdirectory()
# guards above do — no coordinated edit needed when it lands.
if(TARGET velox-nmhost)
install(TARGETS velox-nmhost RUNTIME DESTINATION libexec/velox)
install(FILES ${CMAKE_SOURCE_DIR}/packaging/nativehost/com.velox.host.json
DESTINATION lib/mozilla/native-messaging-hosts)
else()
message(STATUS "velox: nmhost has not landed yet — native-messaging manifest not installed.")
endif()
+129
View File
@@ -0,0 +1,129 @@
.TH VELOX 1 "2026-09-12" "Velox" "User Commands"
.SH NAME
velox \- command-line client for veloxd, the Velox download manager daemon
.SH SYNOPSIS
.B velox
.I command
.RI [ options ]
.SH DESCRIPTION
.B velox
talks to
\fBveloxd\fR
over its Unix-domain socket and gives a scriptable, terminal-first view of the same
downloads the GUI and the Firefox extension see. It does no downloading itself \(em every
command is a thin JSON-RPC call; the daemon owns all state.
.PP
.B veloxd
must already be running (see
.B ENVIRONMENT
below for how
.B velox
finds it, and
.BR systemctl (1)
.RI ( "systemctl --user status velox" )
for whether it's up). If nothing is listening on the socket,
.B velox
exits with status 3 rather than hanging.
.SH COMMANDS
.TP
.BI "add " url " [\-\-dir " dir "] [\-\-out " name "] [\-\-segments " n ]
Add a new download. Prints the new task's id and initial state.
.RS
.TP
.BI "\-\-dir " dir
Destination directory. Must resolve inside one of the daemon's configured
.B saveTo.allowedRoots
or the call fails; omit to use the configured default download directory.
.TP
.BI "\-\-out " name
Filename to save as. Omit to derive one from the URL (or, once the daemon has probed it,
from the server's own
.BR Content-Disposition ).
.TP
.BI "\-\-segments " n
Requested connection count for this download, 1\(en32. The daemon may use fewer \(em a
per-host cap or a source that turns out not to support resuming both lower it. The
.B ls
table (and
.RI "\-\-json's " segments
field) show what was actually granted, not what was asked for.
.RE
.TP
.B ls
List every download the daemon knows about: id, state, progress, and filename. With
.BR \-\-json ", the raw " download.list " result instead of the table."
.TP
.BI "pause " id " [" id " ...]"
Pause one or more downloads by id. A download already paused, or already finished, is
left alone \(em not an error.
.TP
.BI "resume " id " [" id " ...]"
Resume one or more paused downloads.
.TP
.BI "rm " id " [" id " ...] " "[\-\-delete\-file]"
Remove one or more downloads from the list. Without
.BR \-\-delete\-file ,
any partial data on disk
.RI ( .veloxpart / .veloxpart.meta )
is discarded but a
.B completed
file is left in place. With
.BR \-\-delete\-file ,
the finished file is deleted too \(em there is deliberately no default for this flag; you
must say which you mean every time.
.SH OPTIONS
.TP
.B \-\-json
Print the raw JSON-RPC result instead of a formatted table. Works with every command;
combine with
.BR jq (1)
for scripting. On error, the JSON form is an
.B {"error": {...}}
object on stdout rather than a message on stderr.
.TP
.B \-h ", " \-\-help
Print usage and exit 0.
.SH EXIT STATUS
.TP
.B 0
Success.
.TP
.B 1
The daemon reached the call but returned a JSON-RPC error (bad task id, path outside the
allowed roots, and so on). The message is on stderr, or in the JSON error object with
.BR \-\-json .
.TP
.B 2
Usage error \(em missing argument, unknown option, or unknown command.
.TP
.B 3
Could not reach
.B veloxd
at all: not running, or its socket is missing or unreachable.
.SH ENVIRONMENT
.TP
.B XDG_RUNTIME_DIR
.B velox
connects to
.IR "$XDG_RUNTIME_DIR/velox/velox.sock" .
If unset, it falls back to
.IR /run/user/ <uid> ,
matching
\fBveloxd\fR's
own resolution \(em the two must agree for the client to find the daemon, so this is
normally left to the desktop session's default rather than set by hand.
.SH FILES
.TP
.I $XDG_RUNTIME_DIR/velox/velox.sock
The daemon's Unix-domain socket, mode 0600, same-UID only (\fBSO_PEERCRED\fR checked on
every connection \(em this is the transport's authorization, not an extra login).
.SH SEE ALSO
.BR systemctl (1),
.BR jq (1)
.PP
.I docs/01-architecture.md
and
.I docs/agents/AGENT-DAEMON.md
in the Velox source tree for the daemon's own build order and the wire protocol
.B velox
speaks.
+45
View File
@@ -0,0 +1,45 @@
# cmake/platform.cmake — central OS detection and per-OS source selection.
#
# Owned by PKG/QA (docs/adr/0020-cross-platform-strategy.md §4-5, amended: PORT owns
# **/platform/<os>/** and the macOS/Windows packaging trees, but this file is root build
# infrastructure and lands in Phase 0 — CORE's and DAEMON's own seams need
# velox_platform_sources() to select their platform/linux/*.cpp before PORT ever starts).
#
# Sets exactly one of VELOX_OS_LINUX / VELOX_OS_MACOS / VELOX_OS_WINDOWS. Selection lives
# here and nowhere else — no lane's own CMakeLists.txt should reimplement this check.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(VELOX_OS_LINUX TRUE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(VELOX_OS_MACOS TRUE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(VELOX_OS_WINDOWS TRUE)
else()
message(FATAL_ERROR "velox: unsupported CMAKE_SYSTEM_NAME '${CMAKE_SYSTEM_NAME}' — "
"expected Linux, Darwin or Windows.")
endif()
# velox_platform_sources(<target> <dir>) adds <dir>/platform/<os>/*.cpp to <target>, where
# <os> is linux, macos or windows to match this file's VELOX_OS_* selection. <dir> is
# relative to the calling lane's own CMakeLists.txt (e.g. src/io, src/rpc) — the seam
# headers themselves (<dir>/platform/*.hpp) are not globbed here, they're ordinary sources
# the owning lane already lists.
function(velox_platform_sources target dir)
if(VELOX_OS_LINUX)
set(os_dir "linux")
elseif(VELOX_OS_MACOS)
set(os_dir "macos")
elseif(VELOX_OS_WINDOWS)
set(os_dir "windows")
endif()
file(GLOB platform_sources CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/${dir}/platform/${os_dir}/*.cpp")
if(NOT platform_sources)
message(WARNING "velox: velox_platform_sources(${target} ${dir}) found no "
"sources under ${dir}/platform/${os_dir}/ — is the seam missing "
"its ${os_dir} backend?")
endif()
target_sources(${target} PRIVATE ${platform_sources})
endfunction()
+5
View File
@@ -32,6 +32,11 @@ add_library(veloxcore STATIC
) )
add_library(velox::core ALIAS veloxcore) add_library(velox::core ALIAS veloxcore)
# Per-OS backend behind src/io/platform/*.hpp (docs/adr/0020, docs/08-porting.md). Only
# the Linux backend exists (Phase 0); source selection lives in cmake/platform.cmake and
# nowhere else, so PORT's platform/macos/ addition needs no edit here.
velox_platform_sources(veloxcore src/io)
target_include_directories(veloxcore target_include_directories(veloxcore
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src # net/*.cpp -> "net/curl_error.hpp" PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src # net/*.cpp -> "net/curl_error.hpp"
+38
View File
@@ -0,0 +1,38 @@
// vdm/io/platform/file_ops.hpp — platform seam for the three Linux-only file-I/O
// primitives sparse_file.cpp and meta/veloxpart.cpp use (ADR 0020, docs/08-porting.md).
//
// One implementation file per OS under platform/<os>/, selected at build time. No #ifdef
// here, no OS-specific types in the signatures — a reader of the callers must not need to
// know which OS they're on. Linux (platform/linux/file_ops.cpp) is the reference
// implementation and is the only one that exists until the PORT lane adds macOS.
//
// This header compiles standalone.
#ifndef VDM_IO_PLATFORM_FILE_OPS_HPP
#define VDM_IO_PLATFORM_FILE_OPS_HPP
#include <cstdint>
#include "vdm/util/result.hpp"
namespace vdm::io::platform {
// Reserve `bytes` for `fd` so a large transfer doesn't fragment or hit ENOSPC mid-write.
// Linux: posix_fallocate, falling back to ftruncate on EOPNOTSUPP/ENOSYS/EINVAL exactly as
// SparseFile did before this seam existed. Returns whether a true extent-reserving
// preallocation happened (false when the ftruncate fallback was used) so callers can keep
// reporting SparseFile::preallocated() unchanged.
[[nodiscard]] Result<bool> preallocate(int fd, std::uint64_t bytes);
// Drop cached pages for [offset, offset+len) from the OS page cache, best-effort. A
// platform with no equivalent (macOS) is a documented no-op, not a failure — never let
// this block or fail the transfer path.
void advise_dontneed(int fd, std::uint64_t offset, std::uint64_t len) noexcept;
// Flush `fd`'s data to durable storage without waiting on metadata that doesn't affect
// data readback. Linux: fdatasync.
[[nodiscard]] Result<void> flush_durable(int fd);
} // namespace vdm::io::platform
#endif // VDM_IO_PLATFORM_FILE_OPS_HPP
+73
View File
@@ -0,0 +1,73 @@
// vdm/io/platform/linux/file_ops.cpp — Linux backend for the file_ops seam.
//
// Reference implementation (ADR 0020): moved unchanged from sparse_file.cpp and
// meta/veloxpart.cpp. Linux behaviour must never change as a result of a port; if a port
// needs a semantic change here, that's an ADR, not a seam edit.
#include "io/platform/file_ops.hpp"
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <string>
namespace vdm::io::platform {
namespace {
Error errno_to_error(int e) noexcept {
switch (e) {
case ENOSPC:
case EDQUOT:
return Error::disk_full;
case EACCES:
case EPERM:
case EROFS:
return Error::permission_denied;
case ENOENT:
case ENOTDIR:
case EISDIR:
case ENAMETOOLONG:
case ELOOP:
return Error::path_rejected;
default:
return Error::io_error;
}
}
ErrorInfo sys_error(std::string_view what, int e) {
return ErrorInfo(errno_to_error(e), std::string(what) + ": " + std::strerror(e));
}
} // namespace
Result<bool> preallocate(int fd, std::uint64_t bytes) {
// posix_fallocate returns the error number directly and does not set errno.
int rc = ::posix_fallocate(fd, 0, static_cast<off_t>(bytes));
if (rc == 0)
return true;
if (rc == EOPNOTSUPP || rc == ENOSYS || rc == EINVAL) {
if (::ftruncate(fd, static_cast<off_t>(bytes)) != 0)
return sys_error("ftruncate", errno);
return false;
}
return sys_error("posix_fallocate", rc);
}
void advise_dontneed(int fd, std::uint64_t offset, std::uint64_t len) noexcept {
if (len == 0)
return;
::posix_fadvise(fd, static_cast<off_t>(offset), static_cast<off_t>(len), POSIX_FADV_DONTNEED);
}
Result<void> flush_durable(int fd) {
while (::fdatasync(fd) != 0) {
if (errno == EINTR)
continue;
return sys_error("fdatasync", errno);
}
return ok();
}
} // namespace vdm::io::platform
+8 -19
View File
@@ -9,6 +9,8 @@
#include <cstring> #include <cstring>
#include <utility> #include <utility>
#include "io/platform/file_ops.hpp"
namespace vdm::io { namespace vdm::io {
namespace { namespace {
@@ -90,20 +92,12 @@ Result<void> SparseFile::open(std::string_view path, const OpenOptions &opts) {
bool prealloc = false; bool prealloc = false;
if (opts.total_size > 0) { if (opts.total_size > 0) {
if (opts.preallocate) { if (opts.preallocate) {
// posix_fallocate returns the error number directly and does not set errno. Result<bool> r = platform::preallocate(fd, opts.total_size);
int rc = ::posix_fallocate(fd, 0, static_cast<off_t>(opts.total_size)); if (!r.has_value()) {
if (rc == 0) {
prealloc = true;
} else if (rc == EOPNOTSUPP || rc == ENOSYS || rc == EINVAL) {
if (::ftruncate(fd, static_cast<off_t>(opts.total_size)) != 0) {
int e = errno;
::close(fd); ::close(fd);
return sys_error("ftruncate " + p, e); return std::move(r).error();
}
} else {
::close(fd);
return sys_error("posix_fallocate " + p, rc);
} }
prealloc = *r;
} else if (!opts.truncate_existing) { } else if (!opts.truncate_existing) {
// Resuming: make sure the file is at least total_size so pwrite offsets land. // Resuming: make sure the file is at least total_size so pwrite offsets land.
if (::ftruncate(fd, static_cast<off_t>(opts.total_size)) != 0) { if (::ftruncate(fd, static_cast<off_t>(opts.total_size)) != 0) {
@@ -147,18 +141,13 @@ Result<void> SparseFile::write_at(std::uint64_t offset, ConstByteSpan data) {
Result<void> SparseFile::sync() { Result<void> SparseFile::sync() {
if (fd_ < 0) if (fd_ < 0)
return ErrorInfo(Error::internal, "sync on a closed SparseFile"); return ErrorInfo(Error::internal, "sync on a closed SparseFile");
while (::fdatasync(fd_) != 0) { return platform::flush_durable(fd_);
if (errno == EINTR)
continue;
return sys_error("fdatasync", errno);
}
return ok();
} }
void SparseFile::advise_dontneed(std::uint64_t offset, std::uint64_t len) noexcept { void SparseFile::advise_dontneed(std::uint64_t offset, std::uint64_t len) noexcept {
if (fd_ < 0 || len == 0) if (fd_ < 0 || len == 0)
return; return;
::posix_fadvise(fd_, static_cast<off_t>(offset), static_cast<off_t>(len), POSIX_FADV_DONTNEED); platform::advise_dontneed(fd_, offset, len);
} }
Result<void> SparseFile::resize(std::uint64_t size) { Result<void> SparseFile::resize(std::uint64_t size) {
+4 -5
View File
@@ -13,6 +13,7 @@
#include <string> #include <string>
#include <utility> #include <utility>
#include "io/platform/file_ops.hpp"
#include "vdm/util/crc32.hpp" #include "vdm/util/crc32.hpp"
namespace vdm::meta { namespace vdm::meta {
@@ -272,13 +273,11 @@ Result<void> write_veloxpart_file(std::string_view path, const VeloxPart &vp, bo
} }
if (fsync) { if (fsync) {
while (::fdatasync(fd) != 0) { Result<void> r = vdm::io::platform::flush_durable(fd);
if (errno == EINTR) if (!r.has_value()) {
continue;
int e = errno;
::close(fd); ::close(fd);
::unlink(tmp.c_str()); ::unlink(tmp.c_str());
return sys_error("fdatasync " + tmp, e); return ErrorInfo(r.error().code, "fdatasync " + tmp + ": " + r.error().context);
} }
} }
if (::close(fd) != 0) { if (::close(fd) != 0) {
+6 -1
View File
@@ -74,7 +74,7 @@ target_link_libraries(veloxd_sched PUBLIC velox::proto velox::core veloxd_store
# --- veloxd_rpc — the RPC transports + dispatcher ------------------------------------ # --- veloxd_rpc — the RPC transports + dispatcher ------------------------------------
add_library(veloxd_rpc STATIC add_library(veloxd_rpc STATIC
src/rpc/runtime_dir.cpp src/rpc/runtime_dir.cpp
src/rpc/single_instance.cpp src/rpc/systemd_activation.cpp
src/rpc/event_loop.cpp src/rpc/event_loop.cpp
src/rpc/event_hub.cpp src/rpc/event_hub.cpp
src/rpc/uds_server.cpp src/rpc/uds_server.cpp
@@ -86,6 +86,11 @@ add_library(veloxd_rpc STATIC
) )
add_library(velox::daemon_rpc ALIAS veloxd_rpc) add_library(velox::daemon_rpc ALIAS veloxd_rpc)
# Per-OS backends behind daemon/src/rpc/platform/*.hpp (docs/adr/0020, docs/08-porting.md).
# Source selection lives in cmake/platform.cmake and nowhere else — PORT adds
# platform/macos/ with no edit here.
velox_platform_sources(veloxd_rpc src/rpc)
target_include_directories(veloxd_rpc PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) target_include_directories(veloxd_rpc PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_features(veloxd_rpc PUBLIC cxx_std_23) target_compile_features(veloxd_rpc PUBLIC cxx_std_23)
target_compile_options(veloxd_rpc PRIVATE -Wall -Wextra -Wpedantic -Werror) target_compile_options(veloxd_rpc PRIVATE -Wall -Wextra -Wpedantic -Werror)
+3 -1
View File
@@ -7,7 +7,7 @@ close. Kept here (not buried in commit messages) so the next pass can see them a
|---|---|---|---|---| |---|---|---|---|---|
| ~~D7~~ | **Closed — `capture.offer` is real.** Applies `capture.enabled`/`excludedHosts`/`monitoredExtensions`/`monitoredMimeTypes`/`minSizeBytes` from settings, then the rules table (`store::Rules` + CORE's `vdm::rules::match_rules`/`glob_match` — DAEMON only converts its own stored `proto::Rule` JSON into CORE's plain `vdm::rules::Rule` vocabulary, per that header's own layering note), resolves the category folder (a rule's explicit `categoryId`/`saveDir`, else `store::Categories::guess_by_extension` — the same extension-guess `download.probe`'s `suggestedCategoryId` already used, now shared instead of duplicated), dedupes against active (non-terminal) tasks by exact URL, and on `take` calls `add_one()` — the same path `download.add` itself uses — so a captured download is a real, admitted, persisted task, not a special case. The 750 ms deadline (CLAUDE.md §4 / AGENT-DAEMON.md build step 6) is checked cooperatively between every step via a new `rpc::CaptureDataSource` seam (real impl wraps `store::*`; a test fake can jump its own clock forward to simulate "the store was slow just now" with zero real sleep) — catches the realistic failure mode (several slow steps adding up) though it can't preempt one pathologically stuck single call. Verified against real `veloxd` + `tools/testserver`: a monitored-type offer answers in ~5ms and actually creates + downloads the task; an unmonitored type, an excluded host, a rule-vetoed host, and a second offer for a still-active URL all answer `ignore` with the right `reason`; a bad category save dir surfaces its real `-32011` rather than being swallowed. New `capture_offer_test` covers all of the above plus the deadline itself (two cases, one per "slow" checkpoint), asserting real wall-clock time barely moves even though the fake clock jumped 2 simulated seconds — proof the check reads the injected clock, not a disguised sleep. | `rpc/capture_data_source.hpp`, `rpc/dispatcher.{hpp,cpp}`, `store/rules.{hpp,cpp}`, `store/categories.{hpp,cpp}`, `store/tasks.{hpp,cpp}` | — | done | | ~~D7~~ | **Closed — `capture.offer` is real.** Applies `capture.enabled`/`excludedHosts`/`monitoredExtensions`/`monitoredMimeTypes`/`minSizeBytes` from settings, then the rules table (`store::Rules` + CORE's `vdm::rules::match_rules`/`glob_match` — DAEMON only converts its own stored `proto::Rule` JSON into CORE's plain `vdm::rules::Rule` vocabulary, per that header's own layering note), resolves the category folder (a rule's explicit `categoryId`/`saveDir`, else `store::Categories::guess_by_extension` — the same extension-guess `download.probe`'s `suggestedCategoryId` already used, now shared instead of duplicated), dedupes against active (non-terminal) tasks by exact URL, and on `take` calls `add_one()` — the same path `download.add` itself uses — so a captured download is a real, admitted, persisted task, not a special case. The 750 ms deadline (CLAUDE.md §4 / AGENT-DAEMON.md build step 6) is checked cooperatively between every step via a new `rpc::CaptureDataSource` seam (real impl wraps `store::*`; a test fake can jump its own clock forward to simulate "the store was slow just now" with zero real sleep) — catches the realistic failure mode (several slow steps adding up) though it can't preempt one pathologically stuck single call. Verified against real `veloxd` + `tools/testserver`: a monitored-type offer answers in ~5ms and actually creates + downloads the task; an unmonitored type, an excluded host, a rule-vetoed host, and a second offer for a still-active URL all answer `ignore` with the right `reason`; a bad category save dir surfaces its real `-32011` rather than being swallowed. New `capture_offer_test` covers all of the above plus the deadline itself (two cases, one per "slow" checkpoint), asserting real wall-clock time barely moves even though the fake clock jumped 2 simulated seconds — proof the check reads the injected clock, not a disguised sleep. | `rpc/capture_data_source.hpp`, `rpc/dispatcher.{hpp,cpp}`, `store/rules.{hpp,cpp}`, `store/categories.{hpp,cpp}`, `store/tasks.{hpp,cpp}` | — | done |
| ~~D8~~ | **Closed alongside D7**`capture.getRules` returns the same settings-backed `enabled`/`monitoredExtensions`/`monitoredMimeTypes`/`minSizeBytes`/`excludedHosts`/`bypassModifier` capture.offer itself reads, so the two can never drift. `rulesVersion` is a constant `1` — there is no persisted revision counter yet (nothing writes `rules.*` outside this process's own lifetime to need one across a restart), and the extension already re-fetches on `event.settings.changed` regardless of what this number does; noted in case a real counter becomes worth adding later. | `rpc/dispatcher.cpp` | `rulesVersion` is a placeholder constant | — | | ~~D8~~ | **Closed alongside D7**`capture.getRules` returns the same settings-backed `enabled`/`monitoredExtensions`/`monitoredMimeTypes`/`minSizeBytes`/`excludedHosts`/`bypassModifier` capture.offer itself reads, so the two can never drift. `rulesVersion` is a constant `1` — there is no persisted revision counter yet (nothing writes `rules.*` outside this process's own lifetime to need one across a restart), and the extension already re-fetches on `event.settings.changed` regardless of what this number does; noted in case a real counter becomes worth adding later. | `rpc/dispatcher.cpp` | `rulesVersion` is a placeholder constant | — |
| D1 | Pairing prompt is `EnvAutoApprover` (needs `VELOX_PAIR_AUTO=1`) | `rpc/pairing.hpp`, `main.cpp` | A GUI dialog / `org.freedesktop.Notifications` approver is integration work | Build step 7 (systemd + notifications) | | D1 | Pairing prompt is `EnvAutoApprover` (needs `VELOX_PAIR_AUTO=1`) | `rpc/pairing.hpp`, `main.cpp` | A GUI dialog / `org.freedesktop.Notifications` approver is integration work | the `org.freedesktop.Notifications` half of build step 7 — the systemd half closed as D11 below |
| — | **D1, checked this pass, not attempted:** `libdbus-1-dev` (or `libsystemd-dev` for `sd-bus`) has no headers installed in this build environment — only the runtime `.so`s (`dpkg -l`/`apt-cache policy` confirm `libdbus-1-3` present, `libdbus-1-dev` not, "Candidate" available but not installed). A real notification-backed approver needs one of those linked into `veloxd`, which is a new build dependency for `daemon/CMakeLists.txt` (`find_package`/`pkg_check_modules`) and — since packaging manifests need to know about it too — arguably a decision to surface rather than something to reach for silently mid-session. `PairingApprover::approve()` is also still synchronous by shape (its own doc comment already says so: "the real notification-backed approver will run async and is not this shape") — swapping it for the async pattern this session built for `download.probe` (`rpc::TaskActionPort` + the server-layer deferred-reply special-case) is the right shape once there's a real implementation to justify the churn; reshaping the interface with nothing behind it yet would just be churn. Left `EnvAutoApprover` in place rather than build a fragile hand-rolled D-Bus wire client to avoid the missing headers — a broken pairing approver is worse than an honest stub. | `rpc/pairing.hpp` | missing dev headers + an undiscussed new dependency | once `libdbus-1-dev`/`libsystemd-dev` is available and the dependency is approved | | — | **D1, checked this pass, not attempted:** `libdbus-1-dev` (or `libsystemd-dev` for `sd-bus`) has no headers installed in this build environment — only the runtime `.so`s (`dpkg -l`/`apt-cache policy` confirm `libdbus-1-3` present, `libdbus-1-dev` not, "Candidate" available but not installed). A real notification-backed approver needs one of those linked into `veloxd`, which is a new build dependency for `daemon/CMakeLists.txt` (`find_package`/`pkg_check_modules`) and — since packaging manifests need to know about it too — arguably a decision to surface rather than something to reach for silently mid-session. `PairingApprover::approve()` is also still synchronous by shape (its own doc comment already says so: "the real notification-backed approver will run async and is not this shape") — swapping it for the async pattern this session built for `download.probe` (`rpc::TaskActionPort` + the server-layer deferred-reply special-case) is the right shape once there's a real implementation to justify the churn; reshaping the interface with nothing behind it yet would just be churn. Left `EnvAutoApprover` in place rather than build a fragile hand-rolled D-Bus wire client to avoid the missing headers — a broken pairing approver is worse than an honest stub. | `rpc/pairing.hpp` | missing dev headers + an undiscussed new dependency | once `libdbus-1-dev`/`libsystemd-dev` is available and the dependency is approved |
| ~~D2~~ | **Closed**`download.probe` is real on both transports. It's genuinely async (the engine's probe pool, up to the schema's 30s `x-deadlineMs`) and so cannot fit `VeloxDispatcher::on_download_probe`'s synchronous `HandlerResult<T>` return — `uds_server.cpp`/`ws_server.cpp` special-case `"download.probe"` before the generic `dispatch()`, exactly the way they already special-case `session.hello`/`session.subscribe`, and queue the reply whenever the callback fires. `rpc::TaskActionPort::probe_now` (kept in proto/std terms, no `vdm::net::*`, so `veloxd_rpc` never needs `core/include`'s vdm headers) is what both transports call; `sched::Scheduler::probe_now` is the implementation — builds a `vdm::net::ProbeRequest`, runs it on the engine's probe pool, maps a failure to `-32013 ProbeFailed` (with `data.httpStatus` when there was one), and fills `suggestedCategoryId`/`suggestedSaveDir` with a plain extension match against the categories table (not the real rules engine — that's still D3). Verified live: a real probe answers in ~5ms; a bad host maps to `-32013`; a connection issuing a 10s `slow-loris` probe does not block a second connection's `download.list` (answered in ~1ms) — confirms the async design actually keeps the loop free, not just compiles. | `rpc/task_action_port.hpp`, `rpc/{uds_server,ws_server}.{hpp,cpp}`, `sched/scheduler.{cpp,hpp}` | — | done | | ~~D2~~ | **Closed**`download.probe` is real on both transports. It's genuinely async (the engine's probe pool, up to the schema's 30s `x-deadlineMs`) and so cannot fit `VeloxDispatcher::on_download_probe`'s synchronous `HandlerResult<T>` return — `uds_server.cpp`/`ws_server.cpp` special-case `"download.probe"` before the generic `dispatch()`, exactly the way they already special-case `session.hello`/`session.subscribe`, and queue the reply whenever the callback fires. `rpc::TaskActionPort::probe_now` (kept in proto/std terms, no `vdm::net::*`, so `veloxd_rpc` never needs `core/include`'s vdm headers) is what both transports call; `sched::Scheduler::probe_now` is the implementation — builds a `vdm::net::ProbeRequest`, runs it on the engine's probe pool, maps a failure to `-32013 ProbeFailed` (with `data.httpStatus` when there was one), and fills `suggestedCategoryId`/`suggestedSaveDir` with a plain extension match against the categories table (not the real rules engine — that's still D3). Verified live: a real probe answers in ~5ms; a bad host maps to `-32013`; a connection issuing a 10s `slow-loris` probe does not block a second connection's `download.list` (answered in ~1ms) — confirms the async design actually keeps the loop free, not just compiles. | `rpc/task_action_port.hpp`, `rpc/{uds_server,ws_server}.{hpp,cpp}`, `sched/scheduler.{cpp,hpp}` | — | done |
| D3 | Stub handlers for the rest: `grabber.*`, `media.*` | `rpc/dispatcher.cpp` | HLS/DASH grabber and media-variant support don't exist anywhere in this build yet — a bigger feature than a store-wiring pass | M4 territory, per AGENT-DAEMON.md | | D3 | Stub handlers for the rest: `grabber.*`, `media.*` | `rpc/dispatcher.cpp` | HLS/DASH grabber and media-variant support don't exist anywhere in this build yet — a bigger feature than a store-wiring pass | M4 territory, per AGENT-DAEMON.md |
@@ -26,4 +26,6 @@ close. Kept here (not buried in commit messages) so the next pass can see them a
| ~~D4b~~ | **Closed**`download.pause`/`resume`/`start`/`cancel` and `queue.start`/`stop` all drive the scheduler now, and apply *immediately* (not deferred to the next tick — pausing/resuming/cancelling a live transfer can't wait up to 1s, and per ADR 0013 §3 the governor never touches a user-owned pause on its own). New `rpc::TaskActionPort` interface (owned by `rpc/`, implemented by `sched::Scheduler`) is the seam dispatcher.hpp depends on instead of `sched/scheduler.hpp` directly — avoids a real `veloxd_rpc` <-> `veloxd_sched` circular library dependency (`veloxd_sched` already links `veloxd_rpc` for `EventHub`). `Scheduler::user_pause/resume/start/cancel` + `pause_queue` engine-call-then-eager-transition, matching `tick()`'s existing `to_pause` pattern. Fixed a real bug hit while building this: `transition()` always overwrote `pause_reason` to NULL when the engine's own delayed pause-ack callback arrived with no explicit reason, clobbering whatever the actual initiator (user or governor) had just written — now it preserves the stored reason when none is supplied. Verified against real `veloxd` + `tools/testserver`: pausing a live single-segment throttled transfer freezes `downloadedBytes`, resume continues it from that point, cancel stops it; `queue.stop(pauseRunning:true)` pauses the queue's running task immediately. NOTE: `download.start`'s contract "a task in 'queued' jumps its queue" (priority bump) is not implemented — admission is still plain FIFO by `created_at`. | `sched/scheduler.{cpp,hpp}`, `rpc/task_action_port.hpp`, `rpc/dispatcher.{hpp,cpp}`, `store/queues.{cpp,hpp}` | — | done, except the queue-jump priority bump noted above | | ~~D4b~~ | **Closed**`download.pause`/`resume`/`start`/`cancel` and `queue.start`/`stop` all drive the scheduler now, and apply *immediately* (not deferred to the next tick — pausing/resuming/cancelling a live transfer can't wait up to 1s, and per ADR 0013 §3 the governor never touches a user-owned pause on its own). New `rpc::TaskActionPort` interface (owned by `rpc/`, implemented by `sched::Scheduler`) is the seam dispatcher.hpp depends on instead of `sched/scheduler.hpp` directly — avoids a real `veloxd_rpc` <-> `veloxd_sched` circular library dependency (`veloxd_sched` already links `veloxd_rpc` for `EventHub`). `Scheduler::user_pause/resume/start/cancel` + `pause_queue` engine-call-then-eager-transition, matching `tick()`'s existing `to_pause` pattern. Fixed a real bug hit while building this: `transition()` always overwrote `pause_reason` to NULL when the engine's own delayed pause-ack callback arrived with no explicit reason, clobbering whatever the actual initiator (user or governor) had just written — now it preserves the stored reason when none is supplied. Verified against real `veloxd` + `tools/testserver`: pausing a live single-segment throttled transfer freezes `downloadedBytes`, resume continues it from that point, cancel stops it; `queue.stop(pauseRunning:true)` pauses the queue's running task immediately. NOTE: `download.start`'s contract "a task in 'queued' jumps its queue" (priority bump) is not implemented — admission is still plain FIFO by `created_at`. | `sched/scheduler.{cpp,hpp}`, `rpc/task_action_port.hpp`, `rpc/dispatcher.{hpp,cpp}`, `store/queues.{cpp,hpp}` | — | done, except the queue-jump priority bump noted above |
| ~~D5~~ | **Mostly closed**`rpc/event_hub` fans out per-subscription; `session.subscribe` on both transports registers/updates/tears down a real subscription; `Scheduler::transition()` publishes `event.task.state` (with `previousState`) on every state change, scheduler-driven or engine-reported; `dispatcher::on_download_add` publishes `event.task.added`; a 250 ms timer batches `Scheduler::progress_snapshot()` into one `event.task.progress` array per AGENT-DAEMON.md item 5 / the schema's `x-maxRateHz: 4`. Verified live end to end. | — | `event.task.removed` has no source yet (`download.remove` is D3); `event.speed.global`, `event.notify`, `event.auth.required`, `event.settings.changed`, `event.grabber.progress` are unpublished — each lands with its owning handler | as each owning D3 handler lands | | ~~D5~~ | **Mostly closed**`rpc/event_hub` fans out per-subscription; `session.subscribe` on both transports registers/updates/tears down a real subscription; `Scheduler::transition()` publishes `event.task.state` (with `previousState`) on every state change, scheduler-driven or engine-reported; `dispatcher::on_download_add` publishes `event.task.added`; a 250 ms timer batches `Scheduler::progress_snapshot()` into one `event.task.progress` array per AGENT-DAEMON.md item 5 / the schema's `x-maxRateHz: 4`. Verified live end to end. | — | `event.task.removed` has no source yet (`download.remove` is D3); `event.speed.global`, `event.notify`, `event.auth.required`, `event.settings.changed`, `event.grabber.progress` are unpublished — each lands with its owning handler | as each owning D3 handler lands |
| ~~D6~~ | **Closed** — engine numbers now reach the store: `Scheduler::tick()` probes (`EnginePort::probe`) before every `start()`, persisting `sizeBytes`/`resumable`/validators via `Tasks::set_probe_result` before a byte moves; `Scheduler::persist_progress()` (called from `progress_snapshot()` *and* once more from `on_engine_state` right before `release()`/unmap on every terminal transition) writes `downloadedBytes`/`speedBps`/`segments`/`segmentDetail` from the engine's `Progress`, so a task that finishes between two 250 ms ticks (the common case for anything small or fast) still leaves real numbers instead of the pre-persistence defaults. `TaskSummary.segments` is sourced from `segments.size()` when the task has any (matching what actually lands in `segmentDetail`, per the schema's "exactly `segments` entries"), falling back to the engine's `effective_segments` (budget slots *held*, not necessarily physical range count — see `core/include/vdm/task/download.hpp`'s `Progress` comment) only pre-segmentation. `Tasks::set_final_bytes` tops up `on_finished`'s byte count as a last-resort backstop. Migration `0002` adds `speed_bps` to both `tasks` and `segments`, and fixes `segments.state`'s CHECK to include `'pending'` (0001 omitted it, so a pre-connect snapshot could never be written). Verified against real `veloxd` + `tools/testserver` (not just unit tests): `download.list`/`download.get` correct immediately after completion and after a daemon restart. | `sched/scheduler.{cpp,hpp}`, `store/{tasks,segments}.{cpp,hpp}`, `store/migrations/0002_*.sql` | — | done | | ~~D6~~ | **Closed** — engine numbers now reach the store: `Scheduler::tick()` probes (`EnginePort::probe`) before every `start()`, persisting `sizeBytes`/`resumable`/validators via `Tasks::set_probe_result` before a byte moves; `Scheduler::persist_progress()` (called from `progress_snapshot()` *and* once more from `on_engine_state` right before `release()`/unmap on every terminal transition) writes `downloadedBytes`/`speedBps`/`segments`/`segmentDetail` from the engine's `Progress`, so a task that finishes between two 250 ms ticks (the common case for anything small or fast) still leaves real numbers instead of the pre-persistence defaults. `TaskSummary.segments` is sourced from `segments.size()` when the task has any (matching what actually lands in `segmentDetail`, per the schema's "exactly `segments` entries"), falling back to the engine's `effective_segments` (budget slots *held*, not necessarily physical range count — see `core/include/vdm/task/download.hpp`'s `Progress` comment) only pre-segmentation. `Tasks::set_final_bytes` tops up `on_finished`'s byte count as a last-resort backstop. Migration `0002` adds `speed_bps` to both `tasks` and `segments`, and fixes `segments.state`'s CHECK to include `'pending'` (0001 omitted it, so a pre-connect snapshot could never be written). Verified against real `veloxd` + `tools/testserver` (not just unit tests): `download.list`/`download.get` correct immediately after completion and after a daemon restart. | `sched/scheduler.{cpp,hpp}`, `store/{tasks,segments}.{cpp,hpp}`, `store/migrations/0002_*.sql` | — | done |
| ~~D11~~ | **Closed — build order items 7 (the systemd half) and 9: `velox-nmhost`, socket activation, the systemd user units, and `velox(1)`.** `nmhost/src/main.cpp` (185 lines): a `poll()`-driven byte pump between Firefox's native-messaging framing on stdio (4-byte native-byte-order length prefix) and `veloxd`'s own NDJSON framing on the Unix socket — reframes each direction, no JSON parsing, no retry/backoff, exits the moment either side closes. Deliberately dependency-free (no `veloxd_*` library, no `nlohmann_json`) since it runs unconfined outside Firefox's sandbox whatever the packaging format. Two real bugs found and fixed while getting the integration test to actually pass rather than hang: (1) never set the pumped fds non-blocking, so the "drain what's available" read loop blocked on its own second `read()` instead of returning to `poll()`; (2) stdin and stdout are two different descriptors (0 and 1), not one — an early draft polled `POLLOUT` on fd 0, which is opened read-only, so EOF/writability were never both observable through the same `pollfd` entry. Both are exactly the class of bug a "trivial pump" invites and unit tests over the real binary (not just its helper functions) exist specifically to catch. `packaging/nativehost/com.velox.host.json` + its own `README.md` supersede `AGENT-DAEMON.md`'s stale "four locations" (spike S1 / ADR 0003 found only three are real — the fourth, `~/snap/firefox/common/.mozilla/...`, is not read by snap Firefox at all) and spell out the per-user-manifest / postinst implication for PKG/QA. `EnginePort`-style: `rpc/systemd_activation.cpp` is a from-scratch `sd_listen_fds()` (env vars only, no `libsystemd` link — `LISTEN_PID`/`LISTEN_FDS`, fd 3) that `UdsServer::start()` checks first, skipping its own create/bind/chmod/listen when systemd already bound the socket; `packaging/systemd/velox.socket` + `velox.service` are the unit pair, verified both by `systemd-analyze verify` and by an actual fork/dup2/execve simulation of the activation handshake (a real `session.hello` round-tripped over the handed-off fd with no `bind()` ever called inside the daemon for that run). `velox.service` deliberately skips `ProtectSystem=`/`ProtectHome=`/`ReadWritePaths=``saveTo.allowedRoots` is user-configurable to anywhere on the filesystem, and a sandbox here would turn a legitimately-configured save location into an opaque `EROFS`/`EACCES` instead of the daemon's own clear `-32011`. `cli/man/velox.1` documents the CLI as it actually exists today (`add`/`ls`/`pause`/`resume`/`rm`, `--json`, the three-tier `queue`/`settings` subcommands `AGENT-DAEMON.md` build step 8 originally sketched are not implemented in `cli/src/main.cpp` yet, so the page doesn't claim they are) — checked warning-free with `groff -mandoc -ww -z`. | `nmhost/{CMakeLists.txt,src/main.cpp,tests/}`, `daemon/src/rpc/{systemd_activation.{hpp,cpp},uds_server.cpp}`, `packaging/{nativehost,systemd}/`, `cli/man/velox.1` | — | done |
| ~~D12~~ | **Closed — ADR 0020 Phase 0 for `daemon/`.** Pure refactor, zero behaviour change: the four Linux-only surfaces docs/08-porting.md lists for this lane now sit behind seams in `daemon/src/rpc/platform/{wakeup,peer_id,instance_lock,runtime_paths}.hpp`, with today's syscalls moved unchanged into `platform/linux/*.cpp` (`eventfd` -> `platform::Wakeup`, `SO_PEERCRED`/`struct ucred` -> `platform::peer_of` — same-UID check preserved verbatim as the security property, not touched — abstract-namespace socket -> `platform::acquire_instance_lock`, XDG env lookups -> `platform::runtime_base_dir`/`data_base_dir`, with the 0700-and-owned enforcement itself staying portable POSIX logic in `rpc/runtime_dir.cpp` rather than moving). No fifth seam for `timerfd`: both daemon timers (1 s scheduler tick, 250 ms progress batch) now go through a new portable `EventLoop::add_timer`, which folds the next timer deadline into the existing `poll()` timeout instead of a platform timer fd — exactly the case docs/08-porting.md calls out as not needing a backend ("the loop already has a deadline set"), so it stays in `rpc/event_loop.{hpp,cpp}` untouched by lane PORT. `daemon/CMakeLists.txt` lists the four `platform/linux/*.cpp` files directly (no `cmake/platform.cmake` yet — that and `platform/macos/` are PORT's, per the CLAUDE.md lane table added alongside ADR 0020); PORT's Phase 1 replaces the hardcoded list with `velox_platform_sources()`. Old `rpc/single_instance.{hpp,cpp}` deleted (folded into the new seam); `single_instance_test.cpp` retargeted at `rpc/platform/instance_lock.hpp` with no behaviour change. Full suite 59/59 green; `git diff` outside `platform/linux/` is moves/call-site updates only, no `#ifdef` anywhere in `daemon/`. | `rpc/platform/{wakeup,peer_id,instance_lock,runtime_paths}.hpp`, `rpc/platform/linux/*.cpp`, `rpc/event_loop.{hpp,cpp}`, `rpc/{uds_server,runtime_dir}.cpp`, `main.cpp`, `CMakeLists.txt` | — | done |
| — | ~~Observed, not fixed (CORE, not this lane)~~**routed to CORE by the user.** `vdm::task::Progress.speed_bps` reads back as `0` for the whole lifetime of a live, real (non-fake) throttled download, despite `downloadedBytes` visibly advancing between polls — `core/src/task/download_task.cpp`'s per-worker EWMA never seems to produce a nonzero aggregate in this build. DAEMON passes `EnginePort::progress()`'s `speed_bps` straight through (`Scheduler::persist_progress`); nothing in this lane drops it. Still reproduces in the D4b live checks above (0 throughout a paused/resumed/cancelled transfer whose `downloadedBytes` visibly moved) — not re-filed, since it's already CORE's. | | — | ~~Observed, not fixed (CORE, not this lane)~~**routed to CORE by the user.** `vdm::task::Progress.speed_bps` reads back as `0` for the whole lifetime of a live, real (non-fake) throttled download, despite `downloadedBytes` visibly advancing between polls — `core/src/task/download_task.cpp`'s per-worker EWMA never seems to produce a nonzero aggregate in this build. DAEMON passes `EnginePort::progress()`'s `speed_bps` straight through (`Scheduler::persist_progress`); nothing in this lane drops it. Still reproduces in the D4b live checks above (0 throughout a paused/resumed/cancelled transfer whose `downloadedBytes` visibly moved) — not re-filed, since it's already CORE's. |
+8 -35
View File
@@ -5,6 +5,7 @@
// (AGENT-DAEMON.md build order, steps 1 and 3). The scheduler and the engine link land // (AGENT-DAEMON.md build order, steps 1 and 3). The scheduler and the engine link land
// next. // next.
#include <chrono>
#include <csignal> #include <csignal>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
@@ -15,16 +16,14 @@
#include <sys/un.h> #include <sys/un.h>
#include <unistd.h> #include <unistd.h>
#include <sys/timerfd.h>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "rpc/dispatcher.hpp" #include "rpc/dispatcher.hpp"
#include "rpc/event_hub.hpp" #include "rpc/event_hub.hpp"
#include "rpc/event_loop.hpp" #include "rpc/event_loop.hpp"
#include "rpc/pairing.hpp" #include "rpc/pairing.hpp"
#include "rpc/platform/instance_lock.hpp"
#include "rpc/runtime_dir.hpp" #include "rpc/runtime_dir.hpp"
#include "rpc/single_instance.hpp"
#include "rpc/uds_server.hpp" #include "rpc/uds_server.hpp"
#include "rpc/ws_server.hpp" #include "rpc/ws_server.hpp"
#include "sched/engine_port_core.hpp" #include "sched/engine_port_core.hpp"
@@ -56,7 +55,7 @@ int main() {
return 1; return 1;
} }
const int lock_fd = velox::daemon::rpc::acquire_single_instance_lock(rt.path); const int lock_fd = velox::daemon::rpc::platform::acquire_instance_lock(rt.path);
if (lock_fd < 0) { if (lock_fd < 0) {
std::cerr << "veloxd: another instance is already running for this runtime " std::cerr << "veloxd: another instance is already running for this runtime "
"directory (" << rt.path << ")\n"; "directory (" << rt.path << ")\n";
@@ -109,32 +108,15 @@ int main() {
}); });
// A 1 s timer re-runs the scheduler so schedule windows opening/closing and any // A 1 s timer re-runs the scheduler so schedule windows opening/closing and any
// missed nudge are picked up. Registered on the loop, no extra thread. // missed nudge are picked up. Expressed as a poll() timeout behind EventLoop rather
const int tick_fd = ::timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); // than a timerfd (docs/08-porting.md: "the loop already has a deadline set" — no
if (tick_fd >= 0) { // per-OS backend needed here, unlike the other three daemon/rpc seams).
itimerspec spec{}; loop.add_timer(std::chrono::seconds(1), [&] { (void)scheduler.tick(); });
spec.it_value.tv_sec = 1;
spec.it_interval.tv_sec = 1;
::timerfd_settime(tick_fd, 0, &spec, nullptr);
loop.add_fd(tick_fd, velox::daemon::rpc::kRead, [&](int fd, unsigned) {
std::uint64_t ticks = 0;
[[maybe_unused]] ssize_t n = ::read(fd, &ticks, sizeof(ticks));
(void)scheduler.tick();
});
}
// event.task.progress: one array message at <=4 Hz (schema x-maxRateHz), never one // event.task.progress: one array message at <=4 Hz (schema x-maxRateHz), never one
// notification per task (AGENT-DAEMON.md item 5). 250 ms keeps every active task's // notification per task (AGENT-DAEMON.md item 5). 250 ms keeps every active task's
// segment bar under 4 Hz without depending on how many tasks are running. // segment bar under 4 Hz without depending on how many tasks are running.
const int progress_fd = ::timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); loop.add_timer(std::chrono::milliseconds(250), [&] {
if (progress_fd >= 0) {
itimerspec spec{};
spec.it_value.tv_nsec = 250'000'000;
spec.it_interval.tv_nsec = 250'000'000;
::timerfd_settime(progress_fd, 0, &spec, nullptr);
loop.add_fd(progress_fd, velox::daemon::rpc::kRead, [&](int fd, unsigned) {
std::uint64_t ticks = 0;
[[maybe_unused]] ssize_t n = ::read(fd, &ticks, sizeof(ticks));
const auto rows = scheduler.progress_snapshot(); const auto rows = scheduler.progress_snapshot();
if (rows.empty()) return; if (rows.empty()) return;
@@ -159,7 +141,6 @@ int main() {
hub.publish(velox::proto::Event::TaskProgress, hub.publish(velox::proto::Event::TaskProgress,
velox::proto::make_notification(velox::proto::Event::TaskProgress, params)); velox::proto::make_notification(velox::proto::Event::TaskProgress, params));
}); });
}
velox::daemon::rpc::UdsServer uds(loop, dispatcher, hub, rt.socket_path(), &scheduler); velox::daemon::rpc::UdsServer uds(loop, dispatcher, hub, rt.socket_path(), &scheduler);
if (const auto ec = uds.start()) { if (const auto ec = uds.start()) {
@@ -186,14 +167,6 @@ int main() {
loop.run(); loop.run();
std::cout << "veloxd: shutting down\n"; std::cout << "veloxd: shutting down\n";
if (tick_fd >= 0) {
loop.del_fd(tick_fd);
::close(tick_fd);
}
if (progress_fd >= 0) {
loop.del_fd(progress_fd);
::close(progress_fd);
}
g_loop = nullptr; g_loop = nullptr;
::close(lock_fd); ::close(lock_fd);
return 0; return 0;
+44 -21
View File
@@ -1,9 +1,8 @@
#include "rpc/event_loop.hpp" #include "rpc/event_loop.hpp"
#include <poll.h> #include <poll.h>
#include <sys/eventfd.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno> #include <cerrno>
#include <cstdint> #include <cstdint>
#include <stdexcept> #include <stdexcept>
@@ -12,14 +11,10 @@
namespace velox::daemon::rpc { namespace velox::daemon::rpc {
EventLoop::EventLoop() { EventLoop::EventLoop() {
wake_fd_ = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); fds_.emplace(wakeup_.pollfd(), Entry{kRead, [this](int, unsigned) { wakeup_.drain(); }});
if (wake_fd_ < 0) throw std::runtime_error("eventfd() failed");
fds_.emplace(wake_fd_, Entry{kRead, [this](int, unsigned) { drain_wakeup(); }});
} }
EventLoop::~EventLoop() { EventLoop::~EventLoop() = default;
if (wake_fd_ >= 0) ::close(wake_fd_);
}
void EventLoop::add_fd(int fd, unsigned interest, Callback cb) { void EventLoop::add_fd(int fd, unsigned interest, Callback cb) {
fds_[fd] = Entry{interest, std::move(cb)}; fds_[fd] = Entry{interest, std::move(cb)};
@@ -30,15 +25,11 @@ void EventLoop::mod_fd(int fd, unsigned interest) {
} }
void EventLoop::del_fd(int fd) { void EventLoop::del_fd(int fd) {
if (fd == wake_fd_) return; // internal, never removed if (fd == wakeup_.pollfd()) return; // internal, never removed
fds_.erase(fd); fds_.erase(fd);
} }
void EventLoop::wake() noexcept { void EventLoop::wake() noexcept { wakeup_.signal(); }
const std::uint64_t one = 1;
// Best-effort: an EAGAIN here means a wakeup is already pending, which is fine.
[[maybe_unused]] ssize_t n = ::write(wake_fd_, &one, sizeof(one));
}
void EventLoop::stop() noexcept { void EventLoop::stop() noexcept {
stop_requested_ = true; stop_requested_ = true;
@@ -62,9 +53,39 @@ void EventLoop::drain_posts() {
for (auto& fn : batch) fn(); for (auto& fn : batch) fn();
} }
void EventLoop::drain_wakeup() noexcept { EventLoop::TimerId EventLoop::add_timer(std::chrono::milliseconds interval,
std::uint64_t sink = 0; std::function<void()> cb) {
while (::read(wake_fd_, &sink, sizeof(sink)) > 0) { const TimerId id = next_timer_id_++;
timers_.emplace(id, Timer{std::chrono::steady_clock::now() + interval, interval,
std::move(cb)});
wake(); // the loop may already be blocked on a longer timeout
return id;
}
void EventLoop::remove_timer(TimerId id) { timers_.erase(id); }
int EventLoop::next_timeout_ms() const {
if (timers_.empty()) return -1;
auto soonest = timers_.begin()->second.next;
for (const auto& [id, t] : timers_) soonest = std::min(soonest, t.next);
const auto now = std::chrono::steady_clock::now();
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(soonest - now);
return ms.count() > 0 ? static_cast<int>(ms.count()) : 0;
}
void EventLoop::run_due_timers() {
const auto now = std::chrono::steady_clock::now();
// Snapshot ids first: a callback may add/remove timers, which would invalidate
// iteration over timers_ directly.
std::vector<TimerId> due;
for (auto& [id, t] : timers_) {
if (t.next <= now) due.push_back(id);
}
for (const TimerId id : due) {
const auto it = timers_.find(id);
if (it == timers_.end()) continue; // removed by an earlier callback this pass
it->second.next = now + it->second.interval;
it->second.cb();
} }
} }
@@ -83,18 +104,22 @@ void EventLoop::run() {
short ev = 0; short ev = 0;
if (e.interest & kRead) ev |= POLLIN; if (e.interest & kRead) ev |= POLLIN;
if (e.interest & kWrite) ev |= POLLOUT; if (e.interest & kWrite) ev |= POLLOUT;
if (ev == 0 && fd != wake_fd_) continue; if (ev == 0 && fd != wakeup_.pollfd()) continue;
pollfd p{}; pollfd p{};
p.fd = fd; p.fd = fd;
p.events = ev; p.events = ev;
pfds.push_back(p); pfds.push_back(p);
} }
const int rc = ::poll(pfds.data(), pfds.size(), -1); const int rc = ::poll(pfds.data(), pfds.size(), next_timeout_ms());
if (rc < 0) { if (rc < 0) {
if (errno == EINTR) continue; if (errno == EINTR) continue;
throw std::runtime_error("poll() failed"); throw std::runtime_error("poll() failed");
} }
drain_posts();
run_due_timers();
if (rc == 0) continue; if (rc == 0) continue;
// Snapshot the fds that fired before invoking any callback: a callback may erase // Snapshot the fds that fired before invoking any callback: a callback may erase
@@ -104,8 +129,6 @@ void EventLoop::run() {
if (p.revents != 0) fired.push_back(p.fd); if (p.revents != 0) fired.push_back(p.fd);
} }
drain_posts();
for (const int fd : fired) { for (const int fd : fired) {
const auto it = fds_.find(fd); const auto it = fds_.find(fd);
if (it == fds_.end()) continue; // removed by an earlier callback this pass if (it == fds_.end()) continue; // removed by an earlier callback this pass
+27 -3
View File
@@ -10,12 +10,15 @@
// from any thread or a signal handler — they only write() a byte to an internal eventfd. // from any thread or a signal handler — they only write() a byte to an internal eventfd.
#include <atomic> #include <atomic>
#include <chrono>
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
#include <mutex> #include <mutex>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include "rpc/platform/wakeup.hpp"
namespace velox::daemon::rpc { namespace velox::daemon::rpc {
enum Interest : unsigned { enum Interest : unsigned {
@@ -58,22 +61,43 @@ public:
// marshal an engine-thread callback back onto the RPC loop. // marshal an engine-thread callback back onto the RPC loop.
void post(std::function<void()> fn); void post(std::function<void()> fn);
using TimerId = std::uint64_t;
// Fire `cb` roughly every `interval` for as long as the loop runs, expressed as a
// poll(2) timeout rather than a platform timer fd (docs/08-porting.md: "the loop
// already has a deadline set" — no per-OS backend needed for this one). Not
// reentrant-safe to call from inside a timer callback other than the one running.
TimerId add_timer(std::chrono::milliseconds interval, std::function<void()> cb);
void remove_timer(TimerId id);
private: private:
struct Entry { struct Entry {
unsigned interest; unsigned interest;
Callback cb; Callback cb;
}; };
void drain_wakeup() noexcept; struct Timer {
void drain_posts(); std::chrono::steady_clock::time_point next;
std::chrono::milliseconds interval;
std::function<void()> cb;
};
int wake_fd_; // eventfd, always registered void drain_posts();
// Milliseconds until the next timer is due, or -1 if there are none (poll()'s "block
// forever" convention).
int next_timeout_ms() const;
void run_due_timers();
platform::Wakeup wakeup_;
bool running_ = false; bool running_ = false;
std::atomic<bool> stop_requested_ = false; // set from stop(), read by run() std::atomic<bool> stop_requested_ = false; // set from stop(), read by run()
std::unordered_map<int, Entry> fds_; std::unordered_map<int, Entry> fds_;
std::mutex post_mu_; std::mutex post_mu_;
std::vector<std::function<void()>> posts_; std::vector<std::function<void()>> posts_;
TimerId next_timer_id_ = 1;
std::unordered_map<TimerId, Timer> timers_;
}; };
} // namespace velox::daemon::rpc } // namespace velox::daemon::rpc
+20
View File
@@ -0,0 +1,20 @@
#pragma once
// Single-instance guard, keyed by the resolved runtime directory (see rpc/runtime_dir.hpp)
// so isolated instances pointed at different runtime dirs never contend (docs/01 §2). The
// mechanism is Linux's abstract-namespace Unix socket; macOS has no abstract namespace and
// uses a real socket file plus flock() instead, which must unlink a stale socket left by a
// crashed process (docs/08-porting.md "API mapping" — the abstract version got that for
// free from the kernel).
#include <string>
#include <system_error>
namespace velox::daemon::rpc::platform {
// Returns the held fd (kept open for the process lifetime; closing it releases the lock)
// or -1 if another process already holds the lock for this exact `runtime_dir`, or on any
// other error acquiring it.
int acquire_instance_lock(const std::string& runtime_dir);
} // namespace velox::daemon::rpc::platform
@@ -1,4 +1,4 @@
#include "rpc/single_instance.hpp" #include "rpc/platform/instance_lock.hpp"
#include <cstddef> #include <cstddef>
#include <cstring> #include <cstring>
@@ -9,9 +9,9 @@
#include "util/crypto.hpp" #include "util/crypto.hpp"
namespace velox::daemon::rpc { namespace velox::daemon::rpc::platform {
int acquire_single_instance_lock(const std::string& runtime_dir) { int acquire_instance_lock(const std::string& runtime_dir) {
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) return -1; if (fd < 0) return -1;
@@ -35,4 +35,4 @@ int acquire_single_instance_lock(const std::string& runtime_dir) {
return fd; return fd;
} }
} // namespace velox::daemon::rpc } // namespace velox::daemon::rpc::platform
+17
View File
@@ -0,0 +1,17 @@
#include "rpc/platform/peer_id.hpp"
#include <sys/socket.h>
namespace velox::daemon::rpc::platform {
std::error_code peer_of(int fd, PeerId& out) {
ucred cred{};
socklen_t len = sizeof(cred);
if (::getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
return std::error_code(errno, std::generic_category());
}
out.uid = cred.uid;
return {};
}
} // namespace velox::daemon::rpc::platform
@@ -0,0 +1,44 @@
#include "rpc/platform/runtime_paths.hpp"
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
namespace velox::daemon::rpc::platform {
namespace {
std::error_code errc(int e) { return std::error_code(e, std::generic_category()); }
} // namespace
std::error_code runtime_base_dir(std::string& out) {
if (const char* xdg = ::getenv("XDG_RUNTIME_DIR"); xdg != nullptr && xdg[0] != '\0') {
out = xdg;
return {};
}
const std::string base = "/run/user/" + std::to_string(::geteuid());
struct stat st{};
if (::stat(base.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) {
// No XDG_RUNTIME_DIR and no /run/user/<uid>: refuse rather than pick an insecure
// fallback. The caller surfaces this as "cannot start".
return errc(ENOENT);
}
out = base;
return {};
}
std::error_code data_base_dir(std::string& out) {
if (const char* xdg = ::getenv("XDG_DATA_HOME"); xdg != nullptr && xdg[0] != '\0') {
out = xdg;
return {};
}
if (const char* home = ::getenv("HOME"); home != nullptr && home[0] != '\0') {
out = std::string(home) + "/.local/share";
return {};
}
return errc(ENOENT);
}
} // namespace velox::daemon::rpc::platform
+32
View File
@@ -0,0 +1,32 @@
#include "rpc/platform/wakeup.hpp"
#include <sys/eventfd.h>
#include <unistd.h>
#include <cstdint>
#include <stdexcept>
namespace velox::daemon::rpc::platform {
Wakeup::Wakeup() {
fd_ = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (fd_ < 0) throw std::runtime_error("eventfd() failed");
}
Wakeup::~Wakeup() {
if (fd_ >= 0) ::close(fd_);
}
void Wakeup::signal() noexcept {
const std::uint64_t one = 1;
// Best-effort: an EAGAIN here means a wakeup is already pending, which is fine.
[[maybe_unused]] ssize_t n = ::write(fd_, &one, sizeof(one));
}
void Wakeup::drain() noexcept {
std::uint64_t sink = 0;
while (::read(fd_, &sink, sizeof(sink)) > 0) {
}
}
} // namespace velox::daemon::rpc::platform
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// Identifies the process on the other end of a connected Unix-domain socket, for the
// same-UID check that is the Unix transport's authorization boundary (docs/01 §2,
// CLAUDE.md §4). `SO_PEERCRED`/`struct ucred` is Linux-only; macOS has `getpeereid`,
// Windows named pipes carry a token instead (docs/08-porting.md "The seams" /
// "API mapping"). The same-UID check itself is the security property and must not change
// per-OS (docs/adr/0020 decision 2).
#include <system_error>
namespace velox::daemon::rpc::platform {
struct PeerId {
unsigned int uid = 0;
};
// On success, fills `out` with the peer's identity of the already-connected `fd`. On
// failure, `out` is untouched and the error_code explains why (matches errno on Linux).
std::error_code peer_of(int fd, PeerId& out);
} // namespace velox::daemon::rpc::platform
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// Where the OS wants ephemeral runtime state and persistent user data to live, before
// velox appends its own "/velox" subdirectory and applies the shared 0700-and-owned check
// (rpc/runtime_dir.cpp — that enforcement is portable POSIX logic and stays there; only
// "which base directory" is per-OS). Linux: XDG. macOS: $TMPDIR (runtime) and
// ~/Library/Application Support (data) — see docs/08-porting.md "API mapping".
#include <string>
#include <system_error>
namespace velox::daemon::rpc::platform {
// The base directory ephemeral runtime state (sockets, lock files) should live under,
// e.g. "$XDG_RUNTIME_DIR" or "/run/user/<uid>" on Linux. No trailing slash.
std::error_code runtime_base_dir(std::string& out);
// The base directory persistent user data should live under, e.g. "$XDG_DATA_HOME" or
// "~/.local/share" on Linux. No trailing slash.
std::error_code data_base_dir(std::string& out);
} // namespace velox::daemon::rpc::platform
+36
View File
@@ -0,0 +1,36 @@
#pragma once
// The event-loop wakeup primitive (docs/adr/0020, docs/08-porting.md). EventLoop uses this
// to interrupt a blocked poll() from another thread or a signal handler — the eventfd
// mechanics themselves are Linux-only; every other OS backend just needs something
// poll()-able that signal()/drain() can drive the same way (docs/08 §"The seams": a
// self-pipe on macOS, an event object on Windows).
//
// One implementation file per OS under platform/<os>/wakeup.cpp; this header carries no
// OS types and no #ifdef (ADR 0020 decision 1).
namespace velox::daemon::rpc::platform {
class Wakeup {
public:
Wakeup();
~Wakeup();
Wakeup(const Wakeup&) = delete;
Wakeup& operator=(const Wakeup&) = delete;
// The fd to register with poll(2) for readability.
int pollfd() const noexcept { return fd_; }
// Make pollfd() readable. Async-signal-safe and thread-safe.
void signal() noexcept;
// Drain whatever signal() queued so pollfd() stops being readable. Call this from the
// loop thread once pollfd() fires.
void drain() noexcept;
private:
int fd_ = -1;
};
} // namespace velox::daemon::rpc::platform
+4 -19
View File
@@ -5,9 +5,10 @@
#include <unistd.h> #include <unistd.h>
#include <cerrno> #include <cerrno>
#include <cstdlib>
#include <string> #include <string>
#include "rpc/platform/runtime_paths.hpp"
namespace velox::daemon::rpc { namespace velox::daemon::rpc {
namespace { namespace {
@@ -33,17 +34,7 @@ std::error_code ensure_private_dir(const std::string& dir) {
std::error_code resolve_runtime_dir(RuntimeDir& out) { std::error_code resolve_runtime_dir(RuntimeDir& out) {
std::string base; std::string base;
if (const char* xdg = ::getenv("XDG_RUNTIME_DIR"); xdg != nullptr && xdg[0] != '\0') { if (auto ec = platform::runtime_base_dir(base)) return ec;
base = xdg;
} else {
base = "/run/user/" + std::to_string(::geteuid());
struct stat st{};
if (::stat(base.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) {
// No XDG_RUNTIME_DIR and no /run/user/<uid>: we refuse rather than pick an
// insecure fallback. The caller surfaces this as "cannot start".
return errc(ENOENT);
}
}
if (!base.empty() && base.back() == '/') base.pop_back(); if (!base.empty() && base.back() == '/') base.pop_back();
const std::string dir = base + "/velox"; const std::string dir = base + "/velox";
@@ -55,13 +46,7 @@ std::error_code resolve_runtime_dir(RuntimeDir& out) {
std::error_code resolve_data_dir(std::string& out) { std::error_code resolve_data_dir(std::string& out) {
std::string base; std::string base;
if (const char* xdg = ::getenv("XDG_DATA_HOME"); xdg != nullptr && xdg[0] != '\0') { if (auto ec = platform::data_base_dir(base)) return ec;
base = xdg;
} else if (const char* home = ::getenv("HOME"); home != nullptr && home[0] != '\0') {
base = std::string(home) + "/.local/share";
} else {
return errc(ENOENT);
}
if (!base.empty() && base.back() == '/') base.pop_back(); if (!base.empty() && base.back() == '/') base.pop_back();
// Create the XDG base components leniently, then the velox dir with a strict check. // Create the XDG base components leniently, then the velox dir with a strict check.
-27
View File
@@ -1,27 +0,0 @@
#pragma once
// Single-instance guard: bind an abstract-namespace Unix socket whose name is derived
// from the canonical runtime directory (resolve_runtime_dir's result — already the
// per-user default, /run/user/<uid>/velox, unless XDG_RUNTIME_DIR says otherwise). A
// second daemon pointed at the same runtime dir gets EADDRINUSE and exits; one pointed at
// a different (isolated / test) runtime dir gets its own lock and starts fine. The kernel
// reclaims an abstract-namespace address when the holding process dies, so a crash never
// wedges it (docs/01 §2).
//
// Naming this "velox-daemon-<euid>" alone (the old scheme) meant exactly one name per
// user system-wide, so XDG_RUNTIME_DIR isolation never reached it: a leaked test veloxd
// with the same euid held the lock for every isolated instance too, real or test, until
// it was killed. Hashing the resolved runtime dir path instead keeps the real per-user
// daemon unique (its runtime dir is unique to it) while letting isolated instances that
// each point at their own runtime dir coexist.
#include <string>
namespace velox::daemon::rpc {
// Returns the held fd (kept open for the process lifetime; closing it releases the lock)
// or -1 if another process already holds the lock for this exact `runtime_dir`, or on any
// other socket error.
int acquire_single_instance_lock(const std::string& runtime_dir);
} // namespace velox::daemon::rpc
+37
View File
@@ -0,0 +1,37 @@
#include "rpc/systemd_activation.hpp"
#include <unistd.h>
#include <cstdlib>
#include <string>
namespace velox::daemon::rpc {
namespace {
constexpr int kListenFdsStart = 3; // SD_LISTEN_FDS_START
} // namespace
int systemd_activated_fd() {
const char* pid_env = std::getenv("LISTEN_PID");
const char* fds_env = std::getenv("LISTEN_FDS");
int fd = -1;
if (pid_env != nullptr && fds_env != nullptr) {
try {
if (std::stol(pid_env) == static_cast<long>(::getpid()) && std::stol(fds_env) == 1) {
fd = kListenFdsStart;
}
} catch (...) {
// Malformed env from something other than systemd; treat as not activated.
}
}
// Contract: consumed once, then cleared, so a value meant for veloxd is never
// mistaken for one meant for a process it might itself exec later.
::unsetenv("LISTEN_PID");
::unsetenv("LISTEN_FDS");
::unsetenv("LISTEN_FDNAMES");
return fd;
}
} // namespace velox::daemon::rpc
+20
View File
@@ -0,0 +1,20 @@
#pragma once
// Minimal sd_listen_fds(3) reimplementation — one function, no libsystemd dependency, for
// the one fd velox.socket ever hands us. See velox.socket / velox.service in
// packaging/nativehost's systemd unit pair: the socket unit binds
// $XDG_RUNTIME_DIR/velox/velox.sock itself (before veloxd ever runs, so the very first
// connection attempt after boot is queued by the kernel rather than refused) and execs
// veloxd with that listening fd already open at fd 3, LISTEN_FDS=1, LISTEN_PID=<our pid>.
namespace velox::daemon::rpc {
// The systemd-activated listening socket fd, or -1 if this process was not socket-
// activated (LISTEN_PID doesn't match our pid, or LISTEN_FDS is unset/not exactly 1 — more
// than one would mean a unit file mismatch, since veloxd only ever asks for one socket).
// Clears LISTEN_PID/LISTEN_FDS from the environment on the way out either way, per
// sd_listen_fds's own contract, so a value meant for us is never mistaken for one meant for
// a process veloxd might itself exec later.
int systemd_activated_fd();
} // namespace velox::daemon::rpc
+20 -4
View File
@@ -12,7 +12,11 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <fcntl.h>
#include "rpc/event_loop.hpp" #include "rpc/event_loop.hpp"
#include "rpc/platform/peer_id.hpp"
#include "rpc/systemd_activation.hpp"
#include "version.hpp" #include "version.hpp"
namespace velox::daemon::rpc { namespace velox::daemon::rpc {
@@ -74,6 +78,20 @@ UdsServer::~UdsServer() {
} }
std::error_code UdsServer::start() { std::error_code UdsServer::start() {
// velox.socket (systemd user unit, socket activation): the unit binds this path itself
// before veloxd ever runs and hands the already-listening fd over at fd 3 — the first
// connection after boot is queued by the kernel rather than refused, and there is no
// window where a client sees ECONNREFUSED while the daemon is still starting. Skips
// create/bind/chmod/listen entirely; the socket file's lifecycle (including removal on
// stop) belongs to the unit, not to us, so bound_ stays false.
if (const int activated = systemd_activated_fd(); activated >= 0) {
::fcntl(activated, F_SETFL, O_NONBLOCK);
::fcntl(activated, F_SETFD, FD_CLOEXEC);
listen_fd_ = activated;
loop_.add_fd(listen_fd_, kRead, [this](int, unsigned) { on_listener_readable(); });
return {};
}
if (path_.size() + 1 > sizeof(sockaddr_un::sun_path)) return errc(ENAMETOOLONG); if (path_.size() + 1 > sizeof(sockaddr_un::sun_path)) return errc(ENAMETOOLONG);
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
@@ -123,10 +141,8 @@ void UdsServer::on_listener_readable() {
break; // EMFILE/ENFILE: stop accepting this pass; loop retries on next readable break; // EMFILE/ENFILE: stop accepting this pass; loop retries on next readable
} }
ucred cred{}; platform::PeerId peer{};
socklen_t len = sizeof(cred); if (platform::peer_of(cfd, peer) || peer.uid != ::geteuid()) {
if (::getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0 ||
cred.uid != ::geteuid()) {
// Not the same user. The socket mode should already prevent this; refuse hard // Not the same user. The socket mode should already prevent this; refuse hard
// regardless — this is the authorization on the Unix transport (docs/01 §2). // regardless — this is the authorization on the Unix transport (docs/01 §2).
::close(cfd); ::close(cfd);
+1
View File
@@ -24,6 +24,7 @@ veloxd_test(sched_scheduler LIBS veloxd_sched veloxd_rpc)
veloxd_test(event_hub LIBS veloxd_rpc) veloxd_test(event_hub LIBS veloxd_rpc)
veloxd_test(store_categories_queues LIBS veloxd_store) veloxd_test(store_categories_queues LIBS veloxd_store)
veloxd_test(single_instance LIBS veloxd_rpc) veloxd_test(single_instance LIBS veloxd_rpc)
veloxd_test(systemd_activation LIBS veloxd_rpc)
veloxd_test(dispatcher_settings LIBS veloxd_rpc veloxd_store) veloxd_test(dispatcher_settings LIBS veloxd_rpc veloxd_store)
veloxd_test(capture_offer LIBS veloxd_rpc veloxd_store) veloxd_test(capture_offer LIBS veloxd_rpc veloxd_store)
veloxd_test(dispatcher_misc LIBS veloxd_rpc veloxd_store) veloxd_test(dispatcher_misc LIBS veloxd_rpc veloxd_store)
+7 -7
View File
@@ -5,15 +5,15 @@
#include <unistd.h> #include <unistd.h>
#include "check.hpp" #include "check.hpp"
#include "rpc/single_instance.hpp" #include "rpc/platform/instance_lock.hpp"
using namespace velox::daemon::rpc; using namespace velox::daemon::rpc::platform;
void run() { void run() {
// Two different runtime dirs: both acquire the lock independently. // Two different runtime dirs: both acquire the lock independently.
{ {
const int a = acquire_single_instance_lock("/run/user/1000/velox-test-a"); const int a = acquire_instance_lock("/run/user/1000/velox-test-a");
const int b = acquire_single_instance_lock("/run/user/1000/velox-test-b"); const int b = acquire_instance_lock("/run/user/1000/velox-test-b");
CHECK(a >= 0); CHECK(a >= 0);
CHECK(b >= 0); CHECK(b >= 0);
if (a >= 0) ::close(a); if (a >= 0) ::close(a);
@@ -22,16 +22,16 @@ void run() {
// Same runtime dir: the second attempt is refused while the first still holds it. // Same runtime dir: the second attempt is refused while the first still holds it.
{ {
const int first = acquire_single_instance_lock("/run/user/1000/velox-test-shared"); const int first = acquire_instance_lock("/run/user/1000/velox-test-shared");
CHECK(first >= 0); CHECK(first >= 0);
const int second = acquire_single_instance_lock("/run/user/1000/velox-test-shared"); const int second = acquire_instance_lock("/run/user/1000/velox-test-shared");
CHECK(second < 0); CHECK(second < 0);
if (first >= 0) ::close(first); if (first >= 0) ::close(first);
if (second >= 0) ::close(second); if (second >= 0) ::close(second);
// Releasing (closing) the fd frees the abstract-namespace name immediately — a // Releasing (closing) the fd frees the abstract-namespace name immediately — a
// third attempt at the same dir succeeds once the first is gone. // third attempt at the same dir succeeds once the first is gone.
const int third = acquire_single_instance_lock("/run/user/1000/velox-test-shared"); const int third = acquire_instance_lock("/run/user/1000/velox-test-shared");
CHECK(third >= 0); CHECK(third >= 0);
if (third >= 0) ::close(third); if (third >= 0) ::close(third);
} }
+53
View File
@@ -0,0 +1,53 @@
// systemd_activated_fd(): the LISTEN_PID/LISTEN_FDS contract, without a real systemd.
#include <unistd.h>
#include <cstdlib>
#include <string>
#include "check.hpp"
#include "rpc/systemd_activation.hpp"
using namespace velox::daemon::rpc;
namespace {
void set_env(const char* k, const std::string& v) { ::setenv(k, v.c_str(), 1); }
} // namespace
void run() {
// Not activated: neither var set.
::unsetenv("LISTEN_PID");
::unsetenv("LISTEN_FDS");
CHECK_EQ(systemd_activated_fd(), -1);
// LISTEN_PID for a different process: not us, so not activated.
set_env("LISTEN_PID", std::to_string(::getpid() + 1));
set_env("LISTEN_FDS", "1");
CHECK_EQ(systemd_activated_fd(), -1);
// Consumed regardless of the outcome — a stale value from some other process's
// exec chain must not leak into what veloxd checks next time.
CHECK(::getenv("LISTEN_PID") == nullptr);
CHECK(::getenv("LISTEN_FDS") == nullptr);
// Our own pid, LISTEN_FDS=1: activated, fd 3 (SD_LISTEN_FDS_START).
set_env("LISTEN_PID", std::to_string(::getpid()));
set_env("LISTEN_FDS", "1");
CHECK_EQ(systemd_activated_fd(), 3);
CHECK(::getenv("LISTEN_PID") == nullptr);
// Our own pid but LISTEN_FDS=2: a unit file mismatch (veloxd only ever asks for one
// socket) — refuse rather than guess which of two fds is the right one.
set_env("LISTEN_PID", std::to_string(::getpid()));
set_env("LISTEN_FDS", "2");
CHECK_EQ(systemd_activated_fd(), -1);
// Garbage LISTEN_FDS: not activated, not a crash.
set_env("LISTEN_PID", std::to_string(::getpid()));
set_env("LISTEN_FDS", "not-a-number");
CHECK_EQ(systemd_activated_fd(), -1);
::unsetenv("LISTEN_PID");
::unsetenv("LISTEN_FDS");
}
TEST_MAIN()
+11
View File
@@ -0,0 +1,11 @@
velox (0.1.0-1) UNRELEASED; urgency=medium
* Local test build only. Not signed, not uploaded anywhere — see
packaging/README.md for what this package is and is not.
* libveloxcore is linked statically; no libveloxcore.so is shipped.
* Placeholder app icon (packaging/icons/) — replace before any real release.
* No real pairing-approval UI is built yet (needs libdbus-1-dev, not in this
build's Build-Depends): pairing requires VELOX_PAIR_AUTO=1. See
packaging/README.md and postinst's own notice.
-- Velox Local Test Build <[email protected]> Sat, 12 Sep 2026 22:30:43 +0400
+35
View File
@@ -0,0 +1,35 @@
Source: velox
Section: net
Priority: optional
Maintainer: Velox Local Test Build <[email protected]>
Build-Depends: debhelper-compat (= 13),
cmake (>= 3.28),
ninja-build,
pkg-config,
g++ (>= 13),
qt6-base-dev,
qt6-tools-dev,
qt6-tools-dev-tools,
qt6-svg-dev,
libcurl4-openssl-dev,
libssl-dev,
libsqlite3-dev,
nlohmann-json3-dev,
libsecret-1-dev
Standards-Version: 4.6.2
Rules-Requires-Root: no
Package: velox
Architecture: amd64
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: IDM-class download manager for Linux (local test build)
Velox is a segmented, resumable download manager for Linux: a daemon
(veloxd) that does the transfer work, a Qt GUI, a CLI, and a Firefox
extension for capture.
.
This package ships veloxd, the velox CLI, the velox-gui Qt client and
velox-nmhost, the Firefox native-messaging host.
.
THIS IS A LOCAL TEST BUILD, not a release: it uses a placeholder app
icon and has no real pairing-approval UI (see /usr/share/doc/velox, or
packaging/README.md in the source tree, for both limitations).
+26
View File
@@ -0,0 +1,26 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: velox
Source: (no public upstream repository yet — local test build only)
Files: *
Copyright: 2026, the Velox project
License: Unlicensed-TODO
Files: debian/*
Copyright: 2026, the Velox project
License: Unlicensed-TODO
License: Unlicensed-TODO
No license has been chosen for Velox's own source yet. This is a genuine,
open project decision — not something packaging can invent on its own —
and is tracked as a release blocker in packaging/README.md. `lintian` is
expected to flag this package over it (a package normally must declare a
real license); that is correct behaviour on lintian's part, not a bug in
this packaging, and stays that way until the project adopts one.
.
Nothing above changes the licenses of this package's dynamically-linked
runtime dependencies (Qt, libcurl, SQLite, OpenSSL, nlohmann-json,
libsecret and friends) — those remain under their own upstream licenses,
covered by their own packages' Depends, and are not redistributed by this
source package. See packaging/README.md's licence-audit note for what
that still needs before a real release.
Vendored Executable
+92
View File
@@ -0,0 +1,92 @@
#!/bin/sh
# postinst for velox (local test build). See packaging/README.md.
set -e
NM_MANIFEST_SRC=/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json
NM_MANIFEST_NAME=com.velox.host.json
# Every real (human) user with a home directory — UID range matches Debian's own
# adduser default (login.defs UID_MIN=1000), not just "everyone in /home" (a service
# account can have a home dir too) and not root.
real_users() {
getent passwd | awk -F: '$3 >= 1000 && $3 < 60000 && $6 != "" {print $1":"$6}'
}
# Per-user native-messaging manifest (ADR 0003 / docs/05 §4.A): needed for BOTH
# deb/tarball and snap Firefox — the system-wide manifest this package also installs at
# $NM_MANIFEST_SRC only covers deb/tarball Firefox, never snap. Runs unconditionally,
# not just when snap is detected: a per-user login.defs-based install can't assume every
# user runs the same Firefox flavour, and dropping a JSON file nobody reads is harmless.
install_per_user_manifests() {
[ -f "$NM_MANIFEST_SRC" ] || {
echo "velox: native-messaging host not built yet (nmhost lane not landed) —" \
"skipping per-user manifest install."
return 0
}
real_users | while IFS=: read -r user home; do
[ -d "$home" ] || continue
dest_dir="$home/.mozilla/native-messaging-hosts"
install -d -o "$user" -g "$user" -m 0755 "$dest_dir" 2>/dev/null || {
echo "velox: could not create $dest_dir for $user — skipping" >&2
continue
}
cp "$NM_MANIFEST_SRC" "$dest_dir/$NM_MANIFEST_NAME"
chown "$user:$user" "$dest_dir/$NM_MANIFEST_NAME"
chmod 0644 "$dest_dir/$NM_MANIFEST_NAME"
done
}
# docs/07-packaging.md: detect snap Firefox and say so — the extension pairs over
# loopback WebSocket there (ADR 0003), not native messaging from inside the sandbox.
note_snap_firefox() {
if command -v snap >/dev/null 2>&1 && snap list firefox >/dev/null 2>&1; then
cat <<'EOF'
velox: Firefox is installed as a snap on this system. The extension will pair with
veloxd over the loopback WebSocket (ws://127.0.0.1:<port>), not native
messaging — this is expected (see docs/adr/0003, ADR 0003) and needs no
action from you.
EOF
fi
}
case "$1" in
configure)
install_per_user_manifests
note_snap_firefox
cat <<'EOF'
============================================================================
velox: LOCAL TEST BUILD — read this before pairing the extension
============================================================================
No real pairing-approval UI is built into this package yet (it needs
libdbus-1-dev, which this build's dependencies do not include). veloxd's
only pairing approver in this build auto-rejects every request UNLESS you
set VELOX_PAIR_AUTO=1 in its environment — there is no prompt to accept or
decline; it is all-or-nothing. Do not run this on a machine or account
where you would not want every pairing request accepted automatically.
systemctl --user edit velox.service
# add, in the [Service] section:
# Environment=VELOX_PAIR_AUTO=1
debhelper's dh_installsystemduser marks velox.service and velox.socket enabled (the
pair together, via velox.service's Also=velox.socket) for your next login session
automatically. For your CURRENT session, start socket activation now yourself — a
root postinst has no live user session to reach:
systemctl --user daemon-reload
systemctl --user enable --now velox.socket
See packaging/README.md in the source tree for the rest of this build's
testing-only limitations (placeholder icon, no license chosen yet, no
libveloxcore.so — it is linked statically).
============================================================================
EOF
;;
esac
#DEBHELPER#
exit 0
Vendored Executable
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
# postrm for velox (local test build). See packaging/README.md.
set -e
NM_MANIFEST_NAME=com.velox.host.json
real_users() {
getent passwd | awk -F: '$3 >= 1000 && $3 < 60000 && $6 != "" {print $1":"$6}'
}
# Undoes exactly what postinst's install_per_user_manifests() did — never anything
# beyond that file. In particular this never touches a user's Velox data
# ($XDG_DATA_HOME/velox, i.e. normally ~/.local/share/velox — the task database and
# any settings) on either `remove` or `purge`: per-user, root-owned deletion across
# every account on the system is exactly the kind of destructive multi-user operation
# that deserves its own careful design and testing, not a first cut bolted onto this
# local test package. Documented as a deliberate simplification in
# packaging/README.md, not a silent gap.
remove_per_user_manifests() {
real_users | while IFS=: read -r user home; do
f="$home/.mozilla/native-messaging-hosts/$NM_MANIFEST_NAME"
[ -e "$f" ] && rm -f "$f"
done
}
case "$1" in
remove|purge)
remove_per_user_manifests
;;
esac
#DEBHELPER#
exit 0
Vendored Executable
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/make -f
# Velox — local test .deb build. Not a release build (no signing, no LTO tuning beyond
# what the flags below ask for): see packaging/README.md.
#
# Explicit cmake flags rather than a CMakePresets.json preset: dh_auto_configure already
# sets its own CMAKE_INSTALL_PREFIX / build-dir conventions, which fighting a preset's own
# cache variables (build/<preset>/, etc.) would only complicate. The flags mirror the
# `release` preset's intent (RelWithDebInfo, no tests) without inheriting its binaryDir.
export DEB_BUILD_MAINT_OPTIONS = hardening=+all
%:
dh $@
override_dh_auto_configure:
dh_auto_configure -- \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DVELOX_BUILD_TESTS=OFF \
-DVELOX_BUILD_GUI=ON \
-DVELOX_WERROR=OFF
# dh_installsystemd (system units) is disabled: velox.service/velox.socket are --user
# units, not system ones, so that helper has nothing to do here. dh_installsystemduser is
# NOT overridden — it runs normally, finds both units under lib/systemd/user/ in the
# install tree, and marks them enabled for each user's *next* systemd --user login (it
# cannot reach an already-running session from a root maintainer script at install time;
# postinst prints the manual start commands for the current session, matching
# packaging/README.md's own instructions).
override_dh_installsystemd:
+1
View File
@@ -0,0 +1 @@
3.0 (native)
+10
View File
@@ -0,0 +1,10 @@
# initial-upload-closes-no-bugs: this is a local test build (packaging/README.md),
# never intended for the Debian/Ubuntu archive — there is no ITP bug to close.
initial-upload-closes-no-bugs
# maintainer-script-calls-systemctl (postinst): both flagged lines are inside a
# heredoc of instructions printed for the *admin* to run themselves (velox.service is
# a --user unit; postinst has no live user session to invoke systemctl against
# itself). lintian's check is a text scan and can't tell printed advice from an actual
# invocation — verified by reading the built postinst, see packaging/README.md.
maintainer-script-calls-systemctl
+14 -2
View File
@@ -9,16 +9,28 @@ Owner: lane **PKG/QA**. Target: Ubuntu 26.04 LTS.
/usr/bin/velox-gui /usr/bin/velox-gui
/usr/bin/velox # CLI /usr/bin/velox # CLI
/usr/libexec/velox/velox-nmhost # native messaging host /usr/libexec/velox/velox-nmhost # native messaging host
/usr/lib/x86_64-linux-gnu/libveloxcore.so.1
/usr/share/applications/velox.desktop /usr/share/applications/velox.desktop
/usr/share/icons/hicolor/*/apps/velox.png /usr/share/icons/hicolor/*/apps/velox.png
/usr/share/man/man1/velox.1.gz /usr/share/man/man1/velox.1.gz
/usr/lib/systemd/user/velox.service /usr/lib/systemd/user/velox.service
/usr/lib/systemd/user/velox.socket # socket activation /usr/lib/systemd/user/velox.socket # real socket activation, see below
/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json /usr/lib/mozilla/native-messaging-hosts/com.velox.host.json
/etc/xdg/autostart/velox-gui.desktop # optional, off by default /etc/xdg/autostart/velox-gui.desktop # optional, off by default
``` ```
`libveloxcore` is **static**, linked into `veloxd`, `velox` and `velox-gui` directly —
there is no `libveloxcore.so.*` to install. A shared, versioned `libveloxcore.so.1` was
the original plan here; the user decided against building one for now (no consumer needs
it as a shared object yet, and it would need CORE's `core/CMakeLists.txt` to grow a
`SOVERSION`). Revisit as a CORE-lane request if that changes — this doc was corrected in
the same commit as the decision rather than left describing something unshipped.
`velox.service` and `velox.socket` are both DAEMON's (`packaging/systemd/README.md`,
`daemon/src/rpc/systemd_activation.cpp`) — real `sd_listen_fds()` socket activation, not a
plain `exec`. This package installs both units verbatim and lets `dh_installsystemduser`
(compat 13) pick them up from the install tree; see `packaging/README.md` for what that
does and does not do at install time for an already-running session.
`postinst` additionally drops per-user native-messaging manifests for the packaging formats `postinst` additionally drops per-user native-messaging manifests for the packaging formats
that need them, and **detects whether Firefox is a snap** — if so it prints (and the GUI's that need them, and **detects whether Firefox is a snap** — if so it prints (and the GUI's
first-run wizard shows) a one-line note that the extension will pair over loopback. first-run wizard shows) a one-line note that the extension will pair over loopback.
+85
View File
@@ -0,0 +1,85 @@
# 08 — Porting guide (macOS first, Windows later)
Read `docs/adr/0020-cross-platform-strategy.md` first; this file is the concrete inventory
it refers to. **Rule: Linux behaviour never changes. `#ifdef` never appears in logic.**
## Directory layout
Platform backends live beside the code they serve, one directory per OS:
```
core/src/io/platform/linux/file_ops.cpp core/src/io/platform/macos/file_ops.cpp
core/src/io/platform/file_ops.hpp # the seam — no #ifdef, no OS types
daemon/src/rpc/platform/linux/{wakeup,peercred,instance_lock,runtime_dir}.cpp
daemon/src/rpc/platform/macos/{...}.cpp
daemon/src/rpc/platform/*.hpp # the seams
cmake/platform.cmake # VELOX_OS_* + source selection
```
`cmake/platform.cmake` sets exactly one of `VELOX_OS_LINUX`, `VELOX_OS_MACOS`,
`VELOX_OS_WINDOWS`, and exposes `velox_platform_sources(<target> <dir>)` which adds
`<dir>/platform/<os>/*.cpp`. Selection happens there and nowhere else.
## The seams
Five interfaces cover the whole port. Signatures are indicative, not binding — the owning
lane settles them in Phase 0.
| Seam | Interface | Why |
|---|---|---|
| File preallocation | `Result<void> preallocate(int fd, uint64_t bytes)` | `posix_fallocate` is Linux/glibc |
| Cache advice | `void advise_dontneed(int fd, uint64_t off, uint64_t len)` | `posix_fadvise` has no macOS equivalent |
| Durable flush | `Result<void> flush_durable(int fd)` | `fdatasync` vs `F_FULLFSYNC` |
| Loop wakeup | `class Wakeup { int pollfd(); void signal(); void drain(); }` | `eventfd` is Linux-only |
| Peer identity | `Result<PeerId> peer_of(int fd)` | `SO_PEERCRED` vs `LOCAL_PEERCRED` |
| Single instance | `Result<Lock> acquire(string_view runtime_dir)` | abstract sockets are Linux-only |
| Runtime dir | `string runtime_dir()`, `string data_dir()` | XDG vs `~/Library` |
## API mapping
| Linux (today) | macOS | Windows (later) | Note |
|---|---|---|---|
| `posix_fallocate(fd,0,n)` | `fcntl(F_PREALLOCATE)` then `ftruncate(n)` | `SetFileValidData` / `SetEndOfFile` | macOS needs the `ftruncate`; `F_PREALLOCATE` alone does not set size. Fall back to `ftruncate` on failure, exactly as the Linux path already does for `EOPNOTSUPP` |
| `posix_fadvise(DONTNEED)` | no equivalent — **no-op** | `FILE_FLAG_NO_BUFFERING` | Do **not** substitute `F_NOCACHE`: it changes caching for the whole descriptor, not a written range. A no-op is honest; record it |
| `fdatasync(fd)` | `fcntl(fd, F_FULLFSYNC)`, fall back to `fsync` | `FlushFileBuffers` | `fsync` on macOS does **not** guarantee the drive flushed. `.veloxpart` resume integrity depends on this — use `F_FULLFSYNC` |
| `pwrite` | same | `WriteFile` + `OVERLAPPED` | POSIX, no work |
| `O_NOFOLLOW` | same | `FILE_FLAG_OPEN_REPARSE_POINT` | POSIX, no work |
| `eventfd` | self-pipe (`pipe2`/`O_NONBLOCK|O_CLOEXEC`) | event object | `poll(2)` already used, so a pipe read-end drops straight in |
| `timerfd` | `poll()` timeout computed from the next deadline | waitable timer | Simplest port: the loop already has a deadline set |
| `SO_PEERCRED` + `struct ucred` | `getpeereid(fd,&uid,&gid)` | named-pipe token | Same-UID check is the security property; keep it |
| abstract socket `\0velox-daemon-<hash>` | socket file in the runtime dir + `flock(LOCK_EX|LOCK_NB)` | named mutex | macOS has no abstract namespace. Must unlink stale sockets on start — the abstract version got that free |
| `$XDG_RUNTIME_DIR` | `$TMPDIR` (per-user, already private) | `%LOCALAPPDATA%` | macOS has no XDG runtime dir |
| `$XDG_DATA_HOME` | `~/Library/Application Support/Velox` | `%APPDATA%` | |
| `libsecret` / Secret Service | Keychain (`Security.framework`) | DPAPI / Credential Manager | Behind the credential-store seam. CLAUDE.md §4 still applies: never SQLite, never logs |
| `systemd` user units | `launchd` plist (`~/Library/LaunchAgents`) | Service/Task Scheduler | Phase 3 |
## Build dependencies
| Ubuntu | macOS (Homebrew) |
|---|---|
| `qt6-base-dev`, `qt6-svg-dev`, `qt6-tools-dev` | `qt@6` |
| `libcurl4-openssl-dev` | system libcurl, or `curl` |
| `libsqlite3-dev` | system sqlite, or `sqlite` |
| `libssl-dev` | `openssl@3` (set `OPENSSL_ROOT_DIR`) |
| `libsecret-1-dev` | **none** — Keychain is in the SDK |
| `nlohmann-json3-dev` | `nlohmann-json` |
| `nodejs`, `npm` | `node` |
The root `CMakeLists.txt` currently does `pkg_check_modules(LIBSECRET REQUIRED ...)`
unconditionally. That must become Linux-only, or macOS cannot configure at all. It is the
single hard blocker for a first macOS build.
## What does not change
`contracts/` and both generated clients, `tools/mockd`, `tests/conformance`,
`tools/testserver`, the whole extension, and every hostile-mode expectation. If a port
tempts you to change a fixture or a schema, stop — that is a contract change and it goes
through PROTO.
## Verification gates
- **Phase 0 done:** Ubuntu suite still 57/57, and `git diff` shows only moves behind seams.
- **Phase 1 done:** `veloxd`, `velox`, `libveloxcore` build on macOS; core unit tests pass.
- **Phase 2 done:** conformance (mockd **and** live veloxd) and the engine hostile-mode
matrix pass on macOS; one real download completes with a matching SHA-256.
- **Phase 3 done:** `.dmg` or Homebrew formula installs and runs on a clean machine.
+72
View File
@@ -0,0 +1,72 @@
# ADR 0020 — Porting to macOS (and later Windows) without forking the tree
Status: **accepted** · Supersedes nothing · Applies to every lane
## Context
The project was built Ubuntu-first and says so in its own project description. A survey of
`main` (2026-09-15, 57/57 green) found the Linux-specific surface is far smaller than the
Ubuntu-first framing suggests — roughly ten files, all of them already isolated at the
bottom of the stack:
| Area | Linux-only thing | Files |
|---|---|---|
| `core/io` | `posix_fallocate`, `posix_fadvise`, `fdatasync` | `sparse_file.cpp` |
| `core/meta` | `fdatasync` | `veloxpart.cpp` |
| `daemon/rpc` | `eventfd` | `event_loop.{hpp,cpp}`, `main.cpp` |
| `daemon/rpc` | `timerfd` | `main.cpp`, `sched/scheduler.hpp` |
| `daemon/rpc` | `SO_PEERCRED` / `struct ucred` | `uds_server.{hpp,cpp}` |
| `daemon/rpc` | abstract-namespace socket lock | `single_instance.{hpp,cpp}` |
| `daemon/rpc` | `$XDG_RUNTIME_DIR` layout | `runtime_dir.{hpp,cpp}` |
| build | `libsecret-1` is `REQUIRED` at the root | `CMakeLists.txt` |
Everything else is already portable: the event loop is `poll(2)` (POSIX, not `epoll`), the
transfer engine is libcurl, the GUI is Qt 6, `tools/mockd` and `tests/conformance` are
Node, `tools/testserver` is stdlib Python, and the extension is a WebExtension.
## Decision
**One tree, one logic, per-OS backends behind seams.**
1. **No `#ifdef` in logic.** Platform differences live behind a narrow interface, with one
implementation file per OS. A reader of `sparse_file.cpp` must not need to know which OS
they are on. `#ifdef` is allowed only inside a `platform/<os>/` file.
2. **Linux is the reference implementation.** A port may never change Linux behaviour. The
gate is mechanical: the full suite stays green on Ubuntu, and the Linux backend keeps
the same syscalls it uses today. If a port needs a semantic change, that is an ADR of
its own, not a port commit.
3. **Seams are introduced by the owning lane, on Linux, before any port work.** CORE
introduces `core`'s seams; DAEMON introduces `daemon`'s. This is a pure refactor: move
the existing Linux code behind the interface unchanged, prove the suite is still green.
4. **A new lane, PORT, owns only the per-OS backends**`**/platform/<os>/**`. It never
writes logic, and it does not own `cmake/`: the central OS detection module is root
build infrastructure and stays with PKG/QA, who must land it during Phase 0 because
Phase 0's own seams consume it. PORT owning it would deadlock — PORT cannot start until
Phase 0 lands, and Phase 0 cannot select platform sources without it. That keeps CLAUDE.md §1 intact: CORE
still owns `core/`'s logic, DAEMON still owns `daemon/`'s, and macOS work can proceed in
parallel without cross-lane writes.
5. **`VELOX_OS_*` is set once, centrally**, in `cmake/platform.cmake` (owned by PKG/QA),
and platform sources are selected there — not by globbing, not per-target ad hoc.
6. **Optional dependencies are gated, never removed.** `libsecret` stays `REQUIRED` on
Linux and is replaced by Keychain on macOS behind the same credential-store seam.
## Phases
- **Phase 0 — on Ubuntu, by CORE + DAEMON.** Introduce the seams and move today's Linux
code behind them. Zero behaviour change; 57/57 stays green. No macOS code exists yet.
- **Phase 1 — on macOS, by PORT.** Implement the macOS backends until `veloxd`, `velox`
and `libveloxcore` build and their unit tests pass.
- **Phase 2 — parity.** `tools/testserver`, conformance and the engine's hostile-mode
matrix pass on macOS. The GUI builds against Qt 6 for macOS.
- **Phase 3 — packaging.** `.dmg`/Homebrew, `launchd` instead of `systemd`, and the macOS
native-messaging manifest locations. Deliberately last.
## Consequences
- Windows later is the same shape: a third backend directory, no new strategy. The seams
chosen here are POSIX-flavoured but interface-level, so a Win32 backend is additive.
- CI must build both, or macOS rots silently. Until a macOS runner exists, Phase 1 is
verified by hand on the porting machine and the Linux gate stays authoritative.
- The cost is one indirection at the bottom of the I/O and RPC stacks. Measured against
the alternative — `#ifdef` drift, or a forked repo that diverges in a month — it is
cheap.
+5 -1
View File
@@ -43,7 +43,11 @@ Read `contracts/`, `core/include/`, `docs/`. Never write in `core/`, `gui/`, or
output. Build this early: it is how you test the daemon before the GUI exists. output. Build this early: it is how you test the daemon before the GUI exists.
9. **`velox-nmhost`** — 4-byte-length-prefixed stdio ⇄ Unix socket pump. **Under 300 lines, 9. **`velox-nmhost`** — 4-byte-length-prefixed stdio ⇄ Unix socket pump. **Under 300 lines,
zero business logic**, and it must exit cleanly when Firefox closes the pipe. Install zero business logic**, and it must exit cleanly when Firefox closes the pipe. Install
manifests to all four locations listed in `docs/05` §4. manifests to the three real locations in `docs/05` §4 / `docs/adr/0003` — not four:
spike S1 found `~/snap/firefox/common/.mozilla/native-messaging-hosts/` (the intuitive
"inside the snap" path) is not actually read by snap Firefox, and corrected `docs/05` §4
down from its original four-location list. `packaging/nativehost/README.md` has the
current table.
## Definition of done (M1) ## Definition of done (M1)
- Passes the full conformance suite as a server, over **both** transports. - Passes the full conformance suite as a server, over **both** transports.
+51
View File
@@ -0,0 +1,51 @@
# Agent brief — PORT (per-OS backends)
**Starts after Phase 0: CORE and DAEMON have landed the seams on Ubuntu.**
## You own
```
core/src/**/platform/<os>/** daemon/src/**/platform/<os>/**
packaging/macos/** packaging/windows/**
```
You may read everything. **You never write logic** — not in `core/src/io/*.cpp`, not in
`daemon/src/rpc/*.cpp`, not in `gui/`, never in `contracts/`.
## Read first
`docs/adr/0020-cross-platform-strategy.md`, then `docs/08-porting.md` — it has the seam
list, the API mapping and the dependency table. Then `CLAUDE.md`.
## The one rule that matters
**Linux behaviour never changes.** If your port needs a seam that doesn't exist, or a seam
whose shape is wrong, you do not widen it yourself — you file a request with the owning
lane (CORE for `core/`, DAEMON for `daemon/`) exactly as every other lane does. A port that
quietly edits shared logic is how one tree becomes two.
## Build order (macOS)
1. **Configure at all.** The root `CMakeLists.txt` requires `libsecret-1` unconditionally;
that must be Linux-gated before anything else compiles. PKG/QA owns that, and also owns
`cmake/platform.cmake` (the `VELOX_OS_*` detection and `velox_platform_sources()`); both
land in Phase 0, before you start. File anything else you need with them.
2. `core/``preallocate`, `advise_dontneed`, `flush_durable`. Use `F_FULLFSYNC`, not
`fsync`: `.veloxpart` resume correctness depends on a real flush.
3. `daemon/``Wakeup` (self-pipe), `peer_of` (`getpeereid`), `instance_lock` (socket file
+ `flock`, with stale-socket cleanup), `runtime_dir` (`$TMPDIR`, `~/Library/Application
Support/Velox`).
4. Unit tests green for both lanes.
5. `tools/testserver` (stdlib Python) and conformance (Node) — expected to run unchanged.
If they don't, that's a bug worth reporting, not patching around.
6. GUI against Qt 6 for macOS.
7. Packaging — `.dmg`/Homebrew, `launchd`, native-messaging manifest locations. Last.
## Definition of done (Phase 12)
- macOS builds `libveloxcore`, `veloxd` and `velox` with `-Wall -Wextra -Werror`.
- Core and daemon unit tests pass on macOS.
- One real download completes on macOS against `tools/testserver` with a SHA-256 that
matches the server's reference — the same gate the Linux vertical slice passed.
- `kill -9` mid-download, restart, resume completes and the checksum still matches.
- The Ubuntu suite is **still** green from the same commit.
## Do not
- Do not add `#ifdef` outside a `platform/<os>/` file.
- Do not change a fixture, a schema or a hostile-mode expectation to make macOS pass.
- Do not substitute `F_NOCACHE` for `posix_fadvise(DONTNEED)` — see `docs/08` for why.
- Do not start with packaging.
+13
View File
@@ -37,6 +37,7 @@ add_library(velox-gui-lib STATIC
src/tray/TrayIcon.cpp src/tray/TrayIcon.cpp
src/widgets/DropTargetWidget.cpp src/widgets/DropTargetWidget.cpp
src/util/ThemeManager.cpp src/util/ThemeManager.cpp
src/util/IconTheme.cpp
src/util/UiThreadWatchdog.cpp src/util/UiThreadWatchdog.cpp
src/mainwindow/CategoryPanel.cpp src/mainwindow/CategoryPanel.cpp
src/mainwindow/MainWindow.cpp src/mainwindow/MainWindow.cpp
@@ -50,6 +51,18 @@ qt_add_resources(velox-gui-lib "theme"
FILES resources/qss/idm-like.qss resources/qss/dark.qss FILES resources/qss/idm-like.qss resources/qss/dark.qss
) )
# gui/resources/icons/LICENSE: Adwaita-derived (GNOME Project), LGPL-3 or CC-BY-SA-3/4.
# light/dark are the same glyphs recoloured for each toolbar skin (IconTheme picks
# between them the same way ThemeManager picks QSS); app/ is the fixed full-colour
# window/tray glyph.
file(GLOB VELOX_GUI_ICONS_LIGHT resources/icons/light/*.svg)
file(GLOB VELOX_GUI_ICONS_DARK resources/icons/dark/*.svg)
qt_add_resources(velox-gui-lib "icons"
PREFIX "/icons"
BASE "resources/icons"
FILES ${VELOX_GUI_ICONS_LIGHT} ${VELOX_GUI_ICONS_DARK} resources/icons/app/velox.svg
)
target_include_directories(velox-gui-lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) target_include_directories(velox-gui-lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_features(velox-gui-lib PUBLIC cxx_std_23) target_compile_features(velox-gui-lib PUBLIC cxx_std_23)
target_compile_options(velox-gui-lib PRIVATE -Wall -Wextra -Wpedantic -Werror) target_compile_options(velox-gui-lib PRIVATE -Wall -Wextra -Wpedantic -Werror)
+38 -1
View File
@@ -67,7 +67,12 @@ failure for the whole project — not a skipped guard, not a GUI-only problem.
--- ---
## R2 — `--check` verifies the outcome, not the thing that can break ## R2 — `--check` verifies the outcome, not the thing that can break — RESOLVED
**Status: done.** `tools/bootstrap.sh` now validates every apt name in `PKGS` via
`apt-cache policy` and greps for the `Candidate: (none)` line rather than trusting the
exit code (which is 0 either way) — the exact loop this request drafted. This is the
check that would have caught `libqt6svg6-dev` (R1) before the VM did.
`--check` today runs the "verify toolchain" block: `command -v` for the binaries, `--check` today runs the "verify toolchain" block: `command -v` for the binaries,
`pkg-config --exists` for the dev libs (`Qt6Core`, `Qt6Widgets`, `Qt6Svg`, `libcurl`, `pkg-config --exists` for the dev libs (`Qt6Core`, `Qt6Widgets`, `Qt6Svg`, `libcurl`,
@@ -219,3 +224,35 @@ Worth making explicit anyway, since relying on undocumented target leakage is fr
No apt change needed either way — `qt6-base-dev` (already in `APT_GUI`) ships `QtDBus`'s No apt change needed either way — `qt6-base-dev` (already in `APT_GUI`) ships `QtDBus`'s
headers directly (verified live: `dpkg -L qt6-base-dev | grep -i dbus` lists the whole headers directly (verified live: `dpkg -L qt6-base-dev | grep -i dbus` lists the whole
`QtDBus/` include tree). `QtDBus/` include tree).
---
## R5 — real icon set has landed; stop shipping the placeholder, here's the install path
`gui/resources/icons/.gitkeep` is gone. There's a real, licensed icon set now (Adwaita,
GNOME Project, LGPL-3-or-CC-BY-SA — full text and per-file provenance in
`gui/resources/icons/LICENSE`), embedded into `velox-gui` via `qt_add_resources` in
`gui/CMakeLists.txt` (`:/icons/{light,dark}/<name>.svg` for toolbar/menu actions,
`:/icons/app/velox.svg` for the window and tray icon). Nothing needs installing
separately for the binary itself — the SVGs are baked into the Qt resource system at
build time, not read from disk at runtime.
What that means for whatever `.deb` postinst / desktop-entry work PKG is doing:
* **Stop bundling a placeholder icon file in the test `.deb`.** There's no longer a gap
to fill — `velox-gui`'s own binary already carries its icon.
* **App icon for the `.desktop` file / desktop menu entry**, if PKG is generating one:
the source SVG is `gui/resources/icons/app/velox.svg` (full colour, unmodified Adwaita
`folder-download` glyph, same in light and dark). If the packaging pipeline wants a
filesystem-installed icon (standard XDG icon-theme layout, e.g.
`/usr/share/icons/hicolor/scalable/apps/velox.svg`, referenced from the `.desktop`
file's `Icon=velox` key) rather than relying on the binary's embedded window icon for
the taskbar/launcher, that single file is the one to install — no rasterization needed,
it's already a clean scalable SVG. GUI has no packaging-stage code to add this itself
(CLAUDE.md §1: `packaging/` is PKG's lane), so filing the path here rather than reaching
across.
* Licence compliance for the `.deb`: `gui/resources/icons/LICENSE` should ship somewhere
discoverable from the package (e.g. copied into `debian/copyright` or
`/usr/share/doc/velox/copyright` alongside whatever other third-party attributions PKG
already tracks) — it documents the Adwaita provenance and dual LGPL-3/CC-BY-SA license
per file, which needs to be preserved wherever the binary that embeds these SVGs ships.
View File
+66
View File
@@ -0,0 +1,66 @@
Icon set for Velox Download Manager
====================================
Source: GNOME Adwaita icon theme (`adwaita-icon-theme`), version bundled with
Debian/Ubuntu, downloaded from <https://download.gnome.org/sources/adwaita-icon-theme/>.
This is the same "compatibly-licensed set" path called out in
docs/03-gui-spec.md's legal note (Papirus/Breeze-derived is the other option; we used
what was locally available and equally suitable) and gui/docs/agents/AGENT-GUI.md.
Nothing in this directory is IDM's artwork. Files below are recolored (fill colour only —
geometry untouched) derivatives of Adwaita SVGs, laid out in IDM's toolbar positions per
docs/03-gui-spec.md §1, so the *positions* rhyme with IDM's muscle memory while the
*pictures* are GNOME's.
Copyright
---------
(c) 2002-2014 The GNOME Project and Adwaita icon theme contributors, including:
Ulisse Perusin, Riccardo Buzzotta, Josef Vybíral, Hylke Bons, Ricardo González,
Lapo Calamandrei, Rodney Dawes, Luca Ferretti, Tuomas Kuosmanen, Andreas Nilsson,
Jakub Steiner, Claire Alexander, Darren Wilson, and other contributors.
License
-------
Dual-licensed, at the recipient's option:
- GNU Lesser General Public License v3.0 (LGPL-3.0-only), or
- Creative Commons Attribution-Share Alike 3.0 or 4.0 (CC-BY-SA-3.0 / CC-BY-SA-4.0)
Full text: https://www.gnu.org/licenses/lgpl-3.0.html
https://creativecommons.org/licenses/by-sa/4.0/
Attribution: "GNOME Project" (https://www.gnome.org), per the upstream copyright file's
own attribution note.
Provenance and modifications
-----------------------------
All files trace back to `/usr/share/icons/Adwaita/{symbolic,scalable}/...` from the
`adwaita-icon-theme` Debian package. Only the `fill` colour was changed (geometry, paths
and viewBox are untouched) to produce a light-toolbar and a dark-toolbar variant of each
symbolic glyph:
light/*.svg fill #2e2e2e — used when the app is in the idm-like (light) theme
dark/*.svg fill #eeeeec — used when the app is in the dark theme
app/velox.svg unmodified — full-colour "folder-download" glyph, used for the
window icon, the tray icon, and the .desktop icon
Adwaita source file -> our name
-----------------------------------------------------------------------------
symbolic/actions/list-add-symbolic.svg -> add-url.svg
symbolic/actions/media-playback-start-symbolic.svg -> resume.svg
symbolic/actions/media-playback-pause-symbolic.svg -> pause.svg
symbolic/actions/media-playback-stop-symbolic.svg -> stop-all.svg
symbolic/actions/edit-delete-symbolic.svg -> delete.svg
symbolic/actions/edit-clear-all-symbolic.svg -> delete-completed.svg
symbolic/actions/appointment-new-symbolic.svg -> scheduler.svg
symbolic/categories/preferences-system-symbolic.svg -> options.svg
symbolic/legacy/web-browser-symbolic.svg -> grabber.svg
symbolic/actions/document-properties-symbolic.svg -> properties.svg
symbolic/actions/insert-link-symbolic.svg -> batch.svg
scalable/places/folder-download.svg -> app/velox.svg (unmodified)
Toolbar/menu -> icon mapping (docs/03-gui-spec.md §1 toolbar order)
---------------------------------------------------------------------
Add URL -> add-url · Resume -> resume · Pause -> pause · Stop All -> stop-all ·
Delete -> delete · Delete Completed -> delete-completed · Scheduler -> scheduler ·
Options -> options · Grabber -> grabber. Properties and Batch (menu-only, not on the
default toolbar) use properties and batch respectively.
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="128px" viewBox="0 0 128 128" width="128px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<linearGradient id="a" gradientTransform="matrix(0.45451 0 0 0.455522 -1210.292114 616.172607)" gradientUnits="userSpaceOnUse" x1="2689.251953" x2="2918.069824" y1="-1106.802979" y2="-1106.802979">
<stop offset="0" stop-color="#62a0ea"/>
<stop offset="0.0576991" stop-color="#afd4ff"/>
<stop offset="0.122204" stop-color="#62a0ea"/>
<stop offset="0.873306" stop-color="#62a0ea"/>
<stop offset="0.955997" stop-color="#c0d5ea"/>
<stop offset="1" stop-color="#62a0ea"/>
</linearGradient>
<path d="m 21.976562 12 c -5.527343 0 -9.976562 4.460938 -9.976562 10 v 86.03125 c 0 5.542969 4.449219 10 9.976562 10 h 84.042969 c 5.53125 0 9.980469 -4.457031 9.980469 -10 v -72.085938 c 0 -6.628906 -5.359375 -12 -11.972656 -12 h -46.027344 c -2.453125 0 -4.695312 -1.386718 -5.796875 -3.582031 l -1.503906 -2.992187 c -1.65625 -3.292969 -5.019531 -5.371094 -8.699219 -5.371094 z m 0 0" fill="#438de6"/>
<path d="m 65.976562 36 c -2.746093 0 -5.226562 1.101562 -7.027343 2.890625 c -2.273438 2.253906 -5.382813 5.109375 -8.632813 5.109375 h -28.339844 c -5.527343 0 -9.976562 4.460938 -9.976562 10 v 54.03125 c 0 5.542969 4.449219 10 9.976562 10 h 84.042969 c 5.53125 0 9.980469 -4.457031 9.980469 -10 v -62.03125 c 0 -5.539062 -4.449219 -10 -9.980469 -10 z m 0 0" fill="url(#a)"/>
<path d="m 65.976562 32 c -2.746093 0 -5.226562 1.101562 -7.027343 2.890625 c -2.273438 2.253906 -5.382813 5.109375 -8.632813 5.109375 h -28.339844 c -5.527343 0 -9.976562 4.460938 -9.976562 10 v 55.976562 c 0 5.539063 4.449219 10 9.976562 10 h 84.042969 c 5.53125 0 9.980469 -4.460937 9.980469 -10 v -63.976562 c 0 -5.539062 -4.449219 -10 -9.980469 -10 z m 0 0" fill="#a4caee"/>
<path d="m 64 60 c -1.101562 0 -2 0.898438 -2 2 v 17.171875 l -4.585938 -4.585937 c -0.375 -0.375 -0.882812 -0.585938 -1.414062 -0.585938 s -1.039062 0.210938 -1.414062 0.585938 c -0.78125 0.78125 -0.78125 2.046874 0 2.828124 l 8 8 c 0.78125 0.78125 2.046874 0.78125 2.828124 0 l 8 -8 c 0.78125 -0.78125 0.78125 -2.046874 0 -2.828124 s -2.046874 -0.78125 -2.828124 0 l -4.585938 4.585937 v -17.171875 c 0 -1.101562 -0.898438 -2 -2 -2 z m -12 28 v 4 h 24 v -4 z m 0 0" fill="#438de6"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 7 1 v 6 h -6 v 2 h 6 v 6 h 2 v -6 h 6 v -2 h -6 v -6 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="15.980469px" viewBox="0 0 16 15.980469" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#eeeeec">
<path d="m 5 11.976562 v 1 h 0.007812 c -0.003906 0.261719 0.101563 0.515626 0.285157 0.707032 l 2 1.996094 c 0.390625 0.390624 1.023437 0.390624 1.414062 0 l 2 -1.996094 c 0.1875 -0.191406 0.289063 -0.445313 0.289063 -0.707032 h 0.003906 v -1 z m 0 0"/>
<path d="m 7 1.972656 c -1.296875 0 -2.40625 0.839844 -2.824219 2 h 7.652344 c -0.417969 -1.160156 -1.53125 -2 -2.828125 -2 z m -5.996094 3 c -0.558594 0 -1.003906 0.445313 -1.003906 1 c 0 0.554688 0.445312 1 1.003906 1 h 4.996094 c 0.554688 0 1 -0.445312 1 -1 c 0 -0.554687 -0.445312 -1 -1 -1 z m 9 0 c -0.558594 0 -1.003906 0.445313 -1.003906 1 c 0 0.554688 0.445312 1 1.003906 1 h 4.996094 c 0.554688 0 1 -0.445312 1 -1 c 0 -0.554687 -0.445312 -1 -1 -1 z m -5.828125 3 c 0.414063 1.160156 1.527344 1.996094 2.824219 1.996094 h 2 c 1.296875 0 2.414062 -0.835938 2.828125 -1.996094 h -2.792969 c -0.011718 0 -0.023437 0.003906 -0.035156 0.003906 h -2 c -0.011719 0 -0.023438 -0.003906 -0.035156 -0.003906 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#eeeeec">
<path d="m 2 0 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 v -8 h 8 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0 0"/>
<path d="m 8 6 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 h 6 c 1.109375 0 2 -0.890625 2 -2 v -6 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0.003906 2 h 1 h 0.03125 c 0.253906 0.011719 0.507813 0.128906 0.6875 0.3125 l 1.28125 1.28125 l 1.308594 -1.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035156 0.550781 -0.25 0.75 l -1.28125 1.28125 l 1.25 1.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -1.28125 -1.28125 l -1.28125 1.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -0.996094 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 1.277344 -1.25 l -1.277344 -1.28125 c -0.210937 -0.195312 -0.304687 -0.46875 -0.28125 -0.75 z m 0 0"/>
<path d="m 5 3 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 v -8 h 8 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 8 0 c -4.410156 0 -8 3.589844 -8 8 s 3.589844 8 8 8 s 8 -3.589844 8 -8 s -3.589844 -8 -8 -8 z m 0 2 c 3.332031 0 6 2.667969 6 6 s -2.667969 6 -6 6 s -6 -2.667969 -6 -6 s 2.667969 -6 6 -6 z m -2.03125 2.96875 c -0.265625 0 -0.519531 0.105469 -0.707031 0.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 l 1.292969 1.292969 l -1.292969 1.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 s 1.023437 0.390625 1.414062 0 l 1.292969 -1.292969 l 1.292969 1.292969 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 s 0.390625 -1.023437 0 -1.414062 l -1.292969 -1.292969 l 1.292969 -1.292969 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 s -0.519531 0.105469 -0.707031 0.292969 l -1.292969 1.292969 l -1.292969 -1.292969 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16.004"><g color="#000" fill="#eeeeec"><path d="M8 4.846v10.38l-2.344-2.288-1.34 2.734c-.327.74-2.033.145-1.548-.844l1.326-2.839H1.136z" style="marker:none" display="block" overflow="visible"/><path d="M7.156 1.063C3.33 1.522.578 5.02 1.063 8.843c.131 1.035.293 1.383.293 1.383l1.675-1.632A4.992 4.992 0 017.406 3.03a4.992 4.992 0 015.563 4.375c.314 2.608-1.391 5.025-3.969 5.532l.031 2s.521-.106.624-.131c3.416-.834 5.706-4.128 5.283-7.65-.46-3.827-3.955-6.555-7.782-6.095z" style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;marker:none" font-weight="400" font-family="Sans" overflow="visible"/><path d="M4.113 7.815v-.206l-.198.019.05-.393h-.117l-.115.15-.116.056-.165-.093-.017-.206.033-.225.248-.186h.199l.033-.113.247.056.182.225.033-.374.314-.262.116-.28.231-.094.133-.187.297-.056.149-.225h-.446l.28-.13h.199l.28-.094.034-.112-.1-.094-.115-.037.033-.112-.083-.169-.198.075.033-.15-.231-.13-.182.317.017.113-.182.075-.116.243-.05-.225-.313-.13-.05-.169.413-.243.182-.168.017-.206-.1-.056L4.84 4l-.082.206s-.139.027-.174.036C4.13 4.66 3.214 5.562 3 7.266c.008.04.155.268.155.268l.347.206.347.093m3.966-4.3l-.43-.168-.496.056-.611.168-.116.112.38.262v.15l-.149.15.199.392.132-.075.165-.262a5.51 5.51 0 00.727-.28l.199-.505m2.529.342l-.375.094-.219.156v.125l-.375.25.094.344.219-.157.125.157.156.093.094-.28L10 4.5l.063-.094.218-.187h.094l-.094.218v.188c.09-.024.16-.051.25-.063l-.25.188v.125l-.312.219-.281-.063v-.156l-.125.062.062.157h-.219l-.125.218-.156.157-.094.03v.188l.032.157H9.03v.53l.063-.03.094-.219L9.375 6l.031-.094.282-.062.156.187.187.094-.093.187.156-.03.062-.22-.187-.218h.062l.22.156.03.219.157.219.062-.313.094-.031c.096.1.169.231.25.344h.281l.188.125-.094.093-.156.157h-.25l-.344-.094h-.188l-.125.156-.343-.375-.25-.062-.375.062-.157.094V9l.032.031.25-.156.093.094h.281l.125.156-.093.312.187.188V10l.125.25-.093.25c-.01.161 0 .307 0 .469.08.219.143.435.218.656l.063.344v.187h.125l.219-.125h.25l.375-.437-.032-.157.25-.218-.187-.188.219-.187.218-.125.094-.125-.062-.25V9.75l.187-.375.188-.25.25-.563v-.156c-.117.015-.23.023-.344.031-.072.005-.145 0-.219 0a7.442 7.442 0 01-.312-.78l-.157-.188-.093-.313.062-.062.219.25.25.562.156.156-.062.22.156.155.25-.25.312-.218.157-.188v-.219c-.04-.073-.055-.145-.094-.218l-.156.187-.125-.156-.188-.125v-.281l.219.218.219-.03c.101.091.192.207.28.312L13 7.28c0-.174-.2-1.02-.625-1.75S11.22 4.125 11.22 4.125l-.063.094-.218.218-.25-.25h.25l.125-.125-.47-.093-.25-.094z" style="marker:none" overflow="visible" opacity=".3"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 13.855469 0 l -1.539063 1.4375 c -0.453125 0.421875 -0.53125 1.148438 -0.269531 1.707031 l -5.886719 5.996094 c -0.011718 0 -0.019531 0 -0.03125 0 c -0.257812 -0.128906 -0.550781 -0.183594 -0.839844 -0.148437 c -0.328124 0.046874 -0.632812 0.199218 -0.867187 0.441406 l -3.945313 3.996094 c -0.3906245 0.375 -0.5468745 0.933593 -0.4062495 1.457031 c 0.1406255 0.523437 0.5546875 0.929687 1.0820315 1.058593 c 0.527344 0.132813 1.082031 -0.03125 1.453125 -0.425781 l 3.945312 -3.996093 c 0.472657 -0.453126 0.59375 -1.15625 0.296875 -1.738282 l 5.890625 -5.964844 c 0.558594 0.25 1.273438 0.148438 1.707031 -0.289062 l 1.414063 -1.5625 z m -10.308594 0.0898438 c -0.398437 0 -0.785156 0.0937502 -1.140625 0.2187502 l 1.882812 1.878906 c 0.390626 0.382812 0.390626 1 0 1.386719 l -0.710937 0.707031 c -0.386719 0.386719 -1 0.386719 -1.390625 0 l -1.882812 -1.878906 c -0.125 0.355468 -0.2187505 0.742187 -0.2187505 1.140625 c 0 1.90625 1.5507815 3.453125 3.4609375 3.453125 c 0.402344 0 0.789063 -0.09375 1.144531 -0.21875 l 1.175782 1.171875 h 0.058593 l 2.070313 -2.0625 l -1.203125 -1.203125 c 0.125 -0.359375 0.214843 -0.742188 0.214843 -1.140625 c 0 -1.90625 -1.546874 -3.4531252 -3.460937 -3.4531252 z m 6.550781 7.8906252 l -2.070312 2.066406 c 0.011718 0.027344 0.023437 0.058594 0.03125 0.089844 l 1.144531 1.140625 c -0.125 0.355468 -0.21875 0.742187 -0.21875 1.140625 c 0 1.902343 1.550781 3.449219 3.460937 3.449219 c 0.433594 0 0.855469 -0.101563 1.238282 -0.246094 l -2.007813 -2 c -0.386719 -0.386719 -0.386719 -1.035156 0 -1.417969 l 0.679688 -0.679687 c 0.195312 -0.191407 0.457031 -0.308594 0.710937 -0.308594 s 0.515625 0.117187 0.710938 0.308594 l 1.945312 1.941406 c 0.105469 -0.328125 0.183594 -0.683594 0.183594 -1.046875 c 0 -1.90625 -1.546875 -3.453125 -3.460938 -3.453125 c -0.398437 0 -0.785156 0.09375 -1.140624 0.21875 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#eeeeec">
<path d="m 3 1 h 3 c 0.550781 0 1 0.449219 1 1 v 12 c 0 0.550781 -0.449219 1 -1 1 h -3 c -0.550781 0 -1 -0.449219 -1 -1 v -12 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0"/>
<path d="m 10 1 h 3 c 0.550781 0 1 0.449219 1 1 v 12 c 0 0.550781 -0.449219 1 -1 1 h -3 c -0.550781 0 -1 -0.449219 -1 -1 v -12 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 519 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 4 0 c -1.644531 0 -3 1.355469 -3 3 v 10 c 0 1.644531 1.355469 3 3 3 h 3 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 h -3 c -0.570312 0 -1 -0.429688 -1 -1 v -10 c 0 -0.570312 0.429688 -1 1 -1 h 5.585938 l 3.414062 3.414062 v 6.585938 c 0 0.550781 0.449219 1 1 1 s 1 -0.449219 1 -1 v -7 c 0 -0.265625 -0.105469 -0.519531 -0.292969 -0.707031 l -4 -4 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 4.640625 6.167969 c -0.199219 0.003906 -0.402344 0.019531 -0.597656 0.046875 l 1.171875 2.074218 c 0.242187 0.425782 -0.027344 0.914063 -0.539063 1.210938 l -0.925781 0.53125 c -0.515625 0.289062 -1.089844 0.25 -1.332031 -0.175781 l -1.171875 -2.074219 c -0.25 0.308594 -0.476563 0.648438 -0.617188 1.019531 c -0.679687 1.761719 0.285156 3.46875 2.152344 3.804688 c 0.386719 0.074219 0.800781 0.054687 1.191406 0.007812 c 0.03125 -0.007812 1.859375 3.386719 1.859375 3.386719 l 4.167969 0.003906 l -3.199219 -4.957031 c 0.246094 -0.308594 0.445313 -0.652344 0.585938 -1.019531 c 0.679687 -1.765625 -0.285157 -3.472656 -2.148438 -3.808594 c -0.195312 -0.039062 -0.398437 -0.050781 -0.597656 -0.046875 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 2 2.5 v 11 c 0 1.5 1.269531 1.492188 1.269531 1.492188 h 0.128907 c 0.246093 0.003906 0.488281 -0.050782 0.699218 -0.171876 l 9.796875 -5.597656 c 0.433594 -0.242187 0.65625 -0.734375 0.65625 -1.226562 c 0 -0.492188 -0.222656 -0.984375 -0.65625 -1.222656 l -9.796875 -5.597657 c -0.210937 -0.121093 -0.453125 -0.175781 -0.699218 -0.175781 h -0.128907 s -1.269531 0 -1.269531 1.5 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 6.5 0 c -3.578125 0 -6.5 2.921875 -6.5 6.5 s 2.921875 6.5 6.5 6.5 c 0.167969 0 0.335938 -0.007812 0.5 -0.019531 v -2.007813 c -0.164062 0.019532 -0.332031 0.027344 -0.5 0.027344 c -2.496094 0 -4.5 -2.003906 -4.5 -4.5 s 2.003906 -4.5 4.5 -4.5 s 4.5 2.003906 4.5 4.5 c 0 0.167969 -0.007812 0.335938 -0.027344 0.5 h 2.007813 c 0.011719 -0.164062 0.019531 -0.332031 0.019531 -0.5 c 0 -3.578125 -2.921875 -6.5 -6.5 -6.5 z m 0 3 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 2.5 h -1.5 c -0.277344 0 -0.5 0.222656 -0.5 0.5 s 0.222656 0.5 0.5 0.5 h 2 c 0.277344 0 0.5 -0.222656 0.5 -0.5 v -3 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m 4.5 5 v 3 h -3 v 2 h 3 v 3 h 2 v -3 h 3 v -2 h -3 v -3 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 851 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3.5 2 h 9 c 0.828125 0 1.5 0.671875 1.5 1.5 v 9 c 0 0.828125 -0.671875 1.5 -1.5 1.5 h -9 c -0.828125 0 -1.5 -0.671875 -1.5 -1.5 v -9 c 0 -0.828125 0.671875 -1.5 1.5 -1.5 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 345 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 7 1 v 6 h -6 v 2 h 6 v 6 h 2 v -6 h 6 v -2 h -6 v -6 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="15.980469px" viewBox="0 0 16 15.980469" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e2e2e">
<path d="m 5 11.976562 v 1 h 0.007812 c -0.003906 0.261719 0.101563 0.515626 0.285157 0.707032 l 2 1.996094 c 0.390625 0.390624 1.023437 0.390624 1.414062 0 l 2 -1.996094 c 0.1875 -0.191406 0.289063 -0.445313 0.289063 -0.707032 h 0.003906 v -1 z m 0 0"/>
<path d="m 7 1.972656 c -1.296875 0 -2.40625 0.839844 -2.824219 2 h 7.652344 c -0.417969 -1.160156 -1.53125 -2 -2.828125 -2 z m -5.996094 3 c -0.558594 0 -1.003906 0.445313 -1.003906 1 c 0 0.554688 0.445312 1 1.003906 1 h 4.996094 c 0.554688 0 1 -0.445312 1 -1 c 0 -0.554687 -0.445312 -1 -1 -1 z m 9 0 c -0.558594 0 -1.003906 0.445313 -1.003906 1 c 0 0.554688 0.445312 1 1.003906 1 h 4.996094 c 0.554688 0 1 -0.445312 1 -1 c 0 -0.554687 -0.445312 -1 -1 -1 z m -5.828125 3 c 0.414063 1.160156 1.527344 1.996094 2.824219 1.996094 h 2 c 1.296875 0 2.414062 -0.835938 2.828125 -1.996094 h -2.792969 c -0.011718 0 -0.023437 0.003906 -0.035156 0.003906 h -2 c -0.011719 0 -0.023438 -0.003906 -0.035156 -0.003906 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e2e2e">
<path d="m 2 0 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 v -8 h 8 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0 0"/>
<path d="m 8 6 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 h 6 c 1.109375 0 2 -0.890625 2 -2 v -6 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0.003906 2 h 1 h 0.03125 c 0.253906 0.011719 0.507813 0.128906 0.6875 0.3125 l 1.28125 1.28125 l 1.308594 -1.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035156 0.550781 -0.25 0.75 l -1.28125 1.28125 l 1.25 1.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -1.28125 -1.28125 l -1.28125 1.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -0.996094 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 1.277344 -1.25 l -1.277344 -1.28125 c -0.210937 -0.195312 -0.304687 -0.46875 -0.28125 -0.75 z m 0 0"/>
<path d="m 5 3 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 v -8 h 8 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 8 0 c -4.410156 0 -8 3.589844 -8 8 s 3.589844 8 8 8 s 8 -3.589844 8 -8 s -3.589844 -8 -8 -8 z m 0 2 c 3.332031 0 6 2.667969 6 6 s -2.667969 6 -6 6 s -6 -2.667969 -6 -6 s 2.667969 -6 6 -6 z m -2.03125 2.96875 c -0.265625 0 -0.519531 0.105469 -0.707031 0.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 l 1.292969 1.292969 l -1.292969 1.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 s 1.023437 0.390625 1.414062 0 l 1.292969 -1.292969 l 1.292969 1.292969 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 s 0.390625 -1.023437 0 -1.414062 l -1.292969 -1.292969 l 1.292969 -1.292969 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 s -0.519531 0.105469 -0.707031 0.292969 l -1.292969 1.292969 l -1.292969 -1.292969 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16.004"><g color="#000" fill="#2e2e2e"><path d="M8 4.846v10.38l-2.344-2.288-1.34 2.734c-.327.74-2.033.145-1.548-.844l1.326-2.839H1.136z" style="marker:none" display="block" overflow="visible"/><path d="M7.156 1.063C3.33 1.522.578 5.02 1.063 8.843c.131 1.035.293 1.383.293 1.383l1.675-1.632A4.992 4.992 0 017.406 3.03a4.992 4.992 0 015.563 4.375c.314 2.608-1.391 5.025-3.969 5.532l.031 2s.521-.106.624-.131c3.416-.834 5.706-4.128 5.283-7.65-.46-3.827-3.955-6.555-7.782-6.095z" style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;marker:none" font-weight="400" font-family="Sans" overflow="visible"/><path d="M4.113 7.815v-.206l-.198.019.05-.393h-.117l-.115.15-.116.056-.165-.093-.017-.206.033-.225.248-.186h.199l.033-.113.247.056.182.225.033-.374.314-.262.116-.28.231-.094.133-.187.297-.056.149-.225h-.446l.28-.13h.199l.28-.094.034-.112-.1-.094-.115-.037.033-.112-.083-.169-.198.075.033-.15-.231-.13-.182.317.017.113-.182.075-.116.243-.05-.225-.313-.13-.05-.169.413-.243.182-.168.017-.206-.1-.056L4.84 4l-.082.206s-.139.027-.174.036C4.13 4.66 3.214 5.562 3 7.266c.008.04.155.268.155.268l.347.206.347.093m3.966-4.3l-.43-.168-.496.056-.611.168-.116.112.38.262v.15l-.149.15.199.392.132-.075.165-.262a5.51 5.51 0 00.727-.28l.199-.505m2.529.342l-.375.094-.219.156v.125l-.375.25.094.344.219-.157.125.157.156.093.094-.28L10 4.5l.063-.094.218-.187h.094l-.094.218v.188c.09-.024.16-.051.25-.063l-.25.188v.125l-.312.219-.281-.063v-.156l-.125.062.062.157h-.219l-.125.218-.156.157-.094.03v.188l.032.157H9.03v.53l.063-.03.094-.219L9.375 6l.031-.094.282-.062.156.187.187.094-.093.187.156-.03.062-.22-.187-.218h.062l.22.156.03.219.157.219.062-.313.094-.031c.096.1.169.231.25.344h.281l.188.125-.094.093-.156.157h-.25l-.344-.094h-.188l-.125.156-.343-.375-.25-.062-.375.062-.157.094V9l.032.031.25-.156.093.094h.281l.125.156-.093.312.187.188V10l.125.25-.093.25c-.01.161 0 .307 0 .469.08.219.143.435.218.656l.063.344v.187h.125l.219-.125h.25l.375-.437-.032-.157.25-.218-.187-.188.219-.187.218-.125.094-.125-.062-.25V9.75l.187-.375.188-.25.25-.563v-.156c-.117.015-.23.023-.344.031-.072.005-.145 0-.219 0a7.442 7.442 0 01-.312-.78l-.157-.188-.093-.313.062-.062.219.25.25.562.156.156-.062.22.156.155.25-.25.312-.218.157-.188v-.219c-.04-.073-.055-.145-.094-.218l-.156.187-.125-.156-.188-.125v-.281l.219.218.219-.03c.101.091.192.207.28.312L13 7.28c0-.174-.2-1.02-.625-1.75S11.22 4.125 11.22 4.125l-.063.094-.218.218-.25-.25h.25l.125-.125-.47-.093-.25-.094z" style="marker:none" overflow="visible" opacity=".3"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 13.855469 0 l -1.539063 1.4375 c -0.453125 0.421875 -0.53125 1.148438 -0.269531 1.707031 l -5.886719 5.996094 c -0.011718 0 -0.019531 0 -0.03125 0 c -0.257812 -0.128906 -0.550781 -0.183594 -0.839844 -0.148437 c -0.328124 0.046874 -0.632812 0.199218 -0.867187 0.441406 l -3.945313 3.996094 c -0.3906245 0.375 -0.5468745 0.933593 -0.4062495 1.457031 c 0.1406255 0.523437 0.5546875 0.929687 1.0820315 1.058593 c 0.527344 0.132813 1.082031 -0.03125 1.453125 -0.425781 l 3.945312 -3.996093 c 0.472657 -0.453126 0.59375 -1.15625 0.296875 -1.738282 l 5.890625 -5.964844 c 0.558594 0.25 1.273438 0.148438 1.707031 -0.289062 l 1.414063 -1.5625 z m -10.308594 0.0898438 c -0.398437 0 -0.785156 0.0937502 -1.140625 0.2187502 l 1.882812 1.878906 c 0.390626 0.382812 0.390626 1 0 1.386719 l -0.710937 0.707031 c -0.386719 0.386719 -1 0.386719 -1.390625 0 l -1.882812 -1.878906 c -0.125 0.355468 -0.2187505 0.742187 -0.2187505 1.140625 c 0 1.90625 1.5507815 3.453125 3.4609375 3.453125 c 0.402344 0 0.789063 -0.09375 1.144531 -0.21875 l 1.175782 1.171875 h 0.058593 l 2.070313 -2.0625 l -1.203125 -1.203125 c 0.125 -0.359375 0.214843 -0.742188 0.214843 -1.140625 c 0 -1.90625 -1.546874 -3.4531252 -3.460937 -3.4531252 z m 6.550781 7.8906252 l -2.070312 2.066406 c 0.011718 0.027344 0.023437 0.058594 0.03125 0.089844 l 1.144531 1.140625 c -0.125 0.355468 -0.21875 0.742187 -0.21875 1.140625 c 0 1.902343 1.550781 3.449219 3.460937 3.449219 c 0.433594 0 0.855469 -0.101563 1.238282 -0.246094 l -2.007813 -2 c -0.386719 -0.386719 -0.386719 -1.035156 0 -1.417969 l 0.679688 -0.679687 c 0.195312 -0.191407 0.457031 -0.308594 0.710937 -0.308594 s 0.515625 0.117187 0.710938 0.308594 l 1.945312 1.941406 c 0.105469 -0.328125 0.183594 -0.683594 0.183594 -1.046875 c 0 -1.90625 -1.546875 -3.453125 -3.460938 -3.453125 c -0.398437 0 -0.785156 0.09375 -1.140624 0.21875 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e2e2e">
<path d="m 3 1 h 3 c 0.550781 0 1 0.449219 1 1 v 12 c 0 0.550781 -0.449219 1 -1 1 h -3 c -0.550781 0 -1 -0.449219 -1 -1 v -12 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0"/>
<path d="m 10 1 h 3 c 0.550781 0 1 0.449219 1 1 v 12 c 0 0.550781 -0.449219 1 -1 1 h -3 c -0.550781 0 -1 -0.449219 -1 -1 v -12 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 519 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 4 0 c -1.644531 0 -3 1.355469 -3 3 v 10 c 0 1.644531 1.355469 3 3 3 h 3 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 h -3 c -0.570312 0 -1 -0.429688 -1 -1 v -10 c 0 -0.570312 0.429688 -1 1 -1 h 5.585938 l 3.414062 3.414062 v 6.585938 c 0 0.550781 0.449219 1 1 1 s 1 -0.449219 1 -1 v -7 c 0 -0.265625 -0.105469 -0.519531 -0.292969 -0.707031 l -4 -4 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 4.640625 6.167969 c -0.199219 0.003906 -0.402344 0.019531 -0.597656 0.046875 l 1.171875 2.074218 c 0.242187 0.425782 -0.027344 0.914063 -0.539063 1.210938 l -0.925781 0.53125 c -0.515625 0.289062 -1.089844 0.25 -1.332031 -0.175781 l -1.171875 -2.074219 c -0.25 0.308594 -0.476563 0.648438 -0.617188 1.019531 c -0.679687 1.761719 0.285156 3.46875 2.152344 3.804688 c 0.386719 0.074219 0.800781 0.054687 1.191406 0.007812 c 0.03125 -0.007812 1.859375 3.386719 1.859375 3.386719 l 4.167969 0.003906 l -3.199219 -4.957031 c 0.246094 -0.308594 0.445313 -0.652344 0.585938 -1.019531 c 0.679687 -1.765625 -0.285157 -3.472656 -2.148438 -3.808594 c -0.195312 -0.039062 -0.398437 -0.050781 -0.597656 -0.046875 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 2 2.5 v 11 c 0 1.5 1.269531 1.492188 1.269531 1.492188 h 0.128907 c 0.246093 0.003906 0.488281 -0.050782 0.699218 -0.171876 l 9.796875 -5.597656 c 0.433594 -0.242187 0.65625 -0.734375 0.65625 -1.226562 c 0 -0.492188 -0.222656 -0.984375 -0.65625 -1.222656 l -9.796875 -5.597657 c -0.210937 -0.121093 -0.453125 -0.175781 -0.699218 -0.175781 h -0.128907 s -1.269531 0 -1.269531 1.5 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 6.5 0 c -3.578125 0 -6.5 2.921875 -6.5 6.5 s 2.921875 6.5 6.5 6.5 c 0.167969 0 0.335938 -0.007812 0.5 -0.019531 v -2.007813 c -0.164062 0.019532 -0.332031 0.027344 -0.5 0.027344 c -2.496094 0 -4.5 -2.003906 -4.5 -4.5 s 2.003906 -4.5 4.5 -4.5 s 4.5 2.003906 4.5 4.5 c 0 0.167969 -0.007812 0.335938 -0.027344 0.5 h 2.007813 c 0.011719 -0.164062 0.019531 -0.332031 0.019531 -0.5 c 0 -3.578125 -2.921875 -6.5 -6.5 -6.5 z m 0 3 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 2.5 h -1.5 c -0.277344 0 -0.5 0.222656 -0.5 0.5 s 0.222656 0.5 0.5 0.5 h 2 c 0.277344 0 0.5 -0.222656 0.5 -0.5 v -3 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m 4.5 5 v 3 h -3 v 2 h 3 v 3 h 2 v -3 h 3 v -2 h -3 v -3 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 851 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3.5 2 h 9 c 0.828125 0 1.5 0.671875 1.5 1.5 v 9 c 0 0.828125 -0.671875 1.5 -1.5 1.5 h -9 c -0.828125 0 -1.5 -0.671875 -1.5 -1.5 v -9 c 0 -0.828125 0.671875 -1.5 1.5 -1.5 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 345 B

+24
View File
@@ -37,6 +37,7 @@
#include "rpc/RpcClient.hpp" #include "rpc/RpcClient.hpp"
#include "tray/TrayIcon.hpp" #include "tray/TrayIcon.hpp"
#include "util/Format.hpp" #include "util/Format.hpp"
#include "util/IconTheme.hpp"
#include "util/Theme.hpp" #include "util/Theme.hpp"
#include "widgets/DropTargetWidget.hpp" #include "widgets/DropTargetWidget.hpp"
#include "widgets/ProgressDelegate.hpp" #include "widgets/ProgressDelegate.hpp"
@@ -67,8 +68,12 @@ MainWindow::MainWindow(rpc::RpcClient *client, QWidget *parent)
offlineBanner_(new QWidget(this)), offlineBanner_(new QWidget(this)),
countsTimer_(new QTimer(this)) { countsTimer_(new QTimer(this)) {
setWindowTitle(tr("Velox Download Manager")); setWindowTitle(tr("Velox Download Manager"));
setWindowIcon(IconTheme::appIcon());
resize(1040, 600); resize(1040, 600);
iconTheme_ = new IconTheme(this);
connect(iconTheme_, &IconTheme::iconsChanged, this, &MainWindow::applyIcons);
proxy_->setSourceModel(model_); proxy_->setSourceModel(model_);
proxy_->setSortCaseSensitivity(Qt::CaseInsensitive); proxy_->setSortCaseSensitivity(Qt::CaseInsensitive);
@@ -116,6 +121,7 @@ MainWindow::MainWindow(rpc::RpcClient *client, QWidget *parent)
setCentralWidget(central); setCentralWidget(central);
buildActions(); buildActions();
applyIcons();
buildMenus(); buildMenus();
buildToolBar(); buildToolBar();
buildTray(); buildTray();
@@ -205,6 +211,24 @@ void MainWindow::buildActions() {
connect(actShow_, &QAction::triggered, this, &MainWindow::showAndRaise); connect(actShow_, &QAction::triggered, this, &MainWindow::showAndRaise);
} }
// gui/resources/icons/LICENSE: toolbar-position -> icon-stem mapping. Re-run on
// IconTheme::iconsChanged so a live light/dark switch swaps every action's glyph, the
// same way ThemeManager swaps the QSS on the same signal.
void MainWindow::applyIcons() {
actAddUrl_->setIcon(IconTheme::icon(QStringLiteral("add-url")));
actResume_->setIcon(IconTheme::icon(QStringLiteral("resume")));
actPause_->setIcon(IconTheme::icon(QStringLiteral("pause")));
actStop_->setIcon(IconTheme::icon(QStringLiteral("stop-all")));
actRemove_->setIcon(IconTheme::icon(QStringLiteral("delete")));
actResumeAll_->setIcon(IconTheme::icon(QStringLiteral("resume")));
actPauseAll_->setIcon(IconTheme::icon(QStringLiteral("pause")));
actProperties_->setIcon(IconTheme::icon(QStringLiteral("properties")));
actOptions_->setIcon(IconTheme::icon(QStringLiteral("options")));
actScheduler_->setIcon(IconTheme::icon(QStringLiteral("scheduler")));
actBatch_->setIcon(IconTheme::icon(QStringLiteral("batch")));
actGrabber_->setIcon(IconTheme::icon(QStringLiteral("grabber")));
}
void MainWindow::buildMenus() { void MainWindow::buildMenus() {
QMenu *tasks = menuBar()->addMenu(tr("&Tasks")); QMenu *tasks = menuBar()->addMenu(tr("&Tasks"));
tasks->addAction(actAddUrl_); tasks->addAction(actAddUrl_);
+3
View File
@@ -27,6 +27,7 @@ class DownloadTableModel;
class CategoryPanel; class CategoryPanel;
class TrayIcon; class TrayIcon;
class DropTargetWidget; class DropTargetWidget;
class IconTheme;
#ifdef VELOX_GUI_HAVE_DBUS #ifdef VELOX_GUI_HAVE_DBUS
class GlobalShortcut; class GlobalShortcut;
#endif #endif
@@ -69,6 +70,7 @@ class MainWindow : public QMainWindow {
void showAndRaise(); void showAndRaise();
void onSettingsChanged(const QJsonObject &params); void onSettingsChanged(const QJsonObject &params);
void onUrlDropped(const QString &url); void onUrlDropped(const QString &url);
void applyIcons();
private: private:
void buildActions(); void buildActions();
@@ -107,6 +109,7 @@ class MainWindow : public QMainWindow {
QJsonArray categoriesCache_; QJsonArray categoriesCache_;
QJsonArray queuesCache_; QJsonArray queuesCache_;
IconTheme *iconTheme_ = nullptr;
TrayIcon *trayIcon_ = nullptr; TrayIcon *trayIcon_ = nullptr;
DropTargetWidget *dropTarget_ = nullptr; DropTargetWidget *dropTarget_ = nullptr;
bool minimizeToTrayEnabled_ = false; bool minimizeToTrayEnabled_ = false;
+6 -9
View File
@@ -1,29 +1,26 @@
#include "tray/TrayIcon.hpp" #include "tray/TrayIcon.hpp"
#include <QAction> #include <QAction>
#include <QApplication> #include <QCoreApplication>
#include <QJsonObject> #include <QJsonObject>
#include <QMenu> #include <QMenu>
#include <QMessageBox> #include <QMessageBox>
#include <QStyle>
#include <QWidget> #include <QWidget>
#include "dialogs/SpeedLimiterDialog.hpp" #include "dialogs/SpeedLimiterDialog.hpp"
#include "rpc/Protocol.hpp" #include "rpc/Protocol.hpp"
#include "rpc/RpcClient.hpp" #include "rpc/RpcClient.hpp"
#include "util/IconTheme.hpp"
namespace velox::gui { namespace velox::gui {
TrayIcon::TrayIcon(rpc::RpcClient *client, QAction *actShow, QAction *actAddUrl, TrayIcon::TrayIcon(rpc::RpcClient *client, QAction *actShow, QAction *actAddUrl,
QAction *actResumeAll, QAction *actPauseAll, QWidget *parent) QAction *actResumeAll, QAction *actPauseAll, QWidget *parent)
: QSystemTrayIcon(parent), client_(client) { : QSystemTrayIcon(parent), client_(client) {
// No icon set has been designed yet (that's its own build-order step); a standard // gui/resources/icons/LICENSE — same full-colour glyph as the window icon; the tray
// style icon is a placeholder, not a shipped asset. // icon is fixed (not theme-picked) since Adwaita's folder-download already reads
if (auto *w = qobject_cast<QWidget *>(parent)) { // fine against both light and dark shell backgrounds.
setIcon(w->style()->standardIcon(QStyle::SP_ArrowDown)); setIcon(IconTheme::appIcon());
} else {
setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowDown));
}
setToolTip(QCoreApplication::translate("velox::gui::TrayIcon", "Velox Download Manager")); setToolTip(QCoreApplication::translate("velox::gui::TrayIcon", "Velox Download Manager"));
auto *menu = new QMenu(parent); auto *menu = new QMenu(parent);
+27
View File
@@ -0,0 +1,27 @@
#include "util/IconTheme.hpp"
#include <QGuiApplication>
#include <QStyleHints>
namespace velox::gui {
IconTheme::IconTheme(QObject *parent) : QObject(parent) {
connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this,
&IconTheme::onColorSchemeChanged);
}
QIcon IconTheme::icon(const QString &name) {
const bool dark = QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark;
return QIcon(QStringLiteral(":/icons/%1/%2.svg")
.arg(dark ? QStringLiteral("dark") : QStringLiteral("light"), name));
}
QIcon IconTheme::appIcon() {
return QIcon(QStringLiteral(":/icons/app/velox.svg"));
}
void IconTheme::onColorSchemeChanged() {
emit iconsChanged();
}
} // namespace velox::gui
+34
View File
@@ -0,0 +1,34 @@
// Resolves action icons to the light/dark SVG variant matching the current
// QStyleHints::colorScheme(), same trigger ThemeManager uses for the QSS skin. Lane GUI.
// docs/03-gui-spec.md §1/§7, gui/resources/icons/LICENSE.
#pragma once
#include <QIcon>
#include <QObject>
#include <QString>
namespace velox::gui {
class IconTheme : public QObject {
Q_OBJECT
public:
explicit IconTheme(QObject *parent = nullptr);
/// `name` is a stem under gui/resources/icons/{light,dark}/, e.g. "add-url".
static QIcon icon(const QString &name);
/// The fixed full-colour glyph (window icon, tray icon) — same in both themes.
static QIcon appIcon();
signals:
/// Fired on a live light/dark switch; callers holding an icon(name) result should
/// re-fetch and re-apply it.
void iconsChanged();
private slots:
void onColorSchemeChanged();
};
} // namespace velox::gui
+14
View File
@@ -0,0 +1,14 @@
# nmhost/ velox-nmhost, the Firefox native-messaging host. Owned by lane DAEMON.
#
# Deliberately dependency-free: no veloxd_* library, no nlohmann_json, no SQLite. It is a
# byte-level pump between two framings (see src/main.cpp's own header comment) and runs
# unconfined outside Firefox's sandbox (ADR 0003) the less it links, the less there is to
# go wrong running from wherever a snap/deb/flatpak install puts it.
add_executable(velox-nmhost src/main.cpp)
target_compile_features(velox-nmhost PRIVATE cxx_std_23)
target_compile_options(velox-nmhost PRIVATE -Wall -Wextra -Wpedantic -Werror)
if(VELOX_BUILD_TESTS AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/CMakeLists.txt)
add_subdirectory(tests)
endif()
View File
+203
View File
@@ -0,0 +1,203 @@
// velox-nmhost — Firefox native-messaging host. A dumb pump between two framings, nothing
// else: stdin/stdout speak Firefox's own protocol (a 4-byte native-byte-order length
// prefix, then that many bytes of UTF-8 JSON); $XDG_RUNTIME_DIR/velox/velox.sock speaks
// veloxd's own NDJSON (one '\n'-terminated JSON value per line, daemon/src/rpc/ndjson.hpp).
// Reframing between the two is the entire job.
//
// Runs unconfined outside Firefox's snap sandbox with the real $HOME and
// $XDG_RUNTIME_DIR (ADR 0003 §Q2) — see packaging/nativehost/ for the manifest this is
// installed as, and which native-messaging-hosts directory actually gets read by which
// Firefox flavour.
//
// No business logic: no JSON parsing (frames are pure byte spans; only the length prefix
// and the line boundary matter here), no retry/backoff (the extension re-launches a fresh
// host on its own reconnect), no protocol version check (veloxd and the extension settle
// that between themselves once the pump hands their bytes through). Exits the moment
// either side closes.
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cerrno>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <poll.h>
#include <string>
namespace {
// Firefox's own cap (host -> browser) is 1 MiB; this is just a sanity backstop against a
// runaway peer so a malformed stream can't grow a buffer without bound.
constexpr std::size_t kMaxFrameBytes = 8 * 1024 * 1024;
std::string socket_path() {
const char* xdg = std::getenv("XDG_RUNTIME_DIR");
std::string base = (xdg != nullptr && xdg[0] != '\0') ? xdg
: ("/run/user/" + std::to_string(::getuid()));
if (!base.empty() && base.back() == '/') base.pop_back();
return base + "/velox/velox.sock";
}
int connect_socket(const std::string& path) {
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) return -1;
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
if (path.size() + 1 > sizeof(addr.sun_path)) {
::close(fd);
return -1;
}
std::memcpy(addr.sun_path, path.c_str(), path.size());
if (::connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
::close(fd);
return -1;
}
return fd;
}
// Reads everything currently available into `buf`. true on EOF, false otherwise (short
// reads / EAGAIN just return with whatever was appended).
bool read_available(int fd, std::string& buf) {
char chunk[65536];
for (;;) {
const ssize_t n = ::read(fd, chunk, sizeof(chunk));
if (n > 0) {
buf.append(chunk, static_cast<std::size_t>(n));
continue;
}
if (n == 0) return true; // EOF
if (errno == EAGAIN || errno == EWOULDBLOCK) return false;
if (errno == EINTR) continue;
return true; // treat any other error as if the peer hung up
}
}
// Writes as much of `buf` as the fd accepts right now, trimming what was sent. Returns
// false on a hard error (peer gone); EAGAIN is not an error, just "try again later".
bool flush_some(int fd, std::string& buf) {
while (!buf.empty()) {
const ssize_t n = ::write(fd, buf.data(), buf.size());
if (n > 0) {
buf.erase(0, static_cast<std::size_t>(n));
continue;
}
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return true;
if (n < 0 && errno == EINTR) continue;
return false;
}
return true;
}
// stdin frames (4-byte length + payload) -> NDJSON lines appended to `to_socket`.
// Malformed (oversized) length is a hard stop.
bool drain_stdin_frames(std::string& in, std::string& to_socket) {
for (;;) {
if (in.size() < 4) return true;
std::uint32_t len;
std::memcpy(&len, in.data(), 4);
if (len > kMaxFrameBytes) return false;
if (in.size() < 4 + len) return true;
to_socket.append(in, 4, len);
to_socket.push_back('\n');
in.erase(0, 4 + len);
}
}
// NDJSON lines from the socket -> stdout frames (4-byte length + payload) appended to
// `to_stdout`.
bool drain_socket_lines(std::string& in, std::string& to_stdout) {
for (;;) {
const auto nl = in.find('\n');
if (nl == std::string::npos) {
if (in.size() > kMaxFrameBytes) return false;
return true;
}
std::string_view line(in.data(), nl);
if (!line.empty() && line.back() == '\r') line.remove_suffix(1);
if (line.size() > kMaxFrameBytes) return false;
const auto len = static_cast<std::uint32_t>(line.size());
to_stdout.append(reinterpret_cast<const char*>(&len), 4);
to_stdout.append(line);
in.erase(0, nl + 1);
}
}
} // namespace
int main() {
const int sock = connect_socket(socket_path());
if (sock < 0) return 1; // daemon not running / socket missing: nothing to pump
// Every fd this pumps must be non-blocking: read_available()'s own drain loop keeps
// calling read() until it actually sees EAGAIN, and a blocking fd never returns that —
// it just blocks inside the "drain what's available" loop instead of going back to
// poll(), which stalls the whole pump the moment one side has more to send than fits
// in a single read().
::fcntl(0, F_SETFL, ::fcntl(0, F_GETFL) | O_NONBLOCK);
::fcntl(1, F_SETFL, ::fcntl(1, F_GETFL) | O_NONBLOCK);
::fcntl(sock, F_SETFL, ::fcntl(sock, F_GETFL) | O_NONBLOCK);
std::string stdin_buf, stdout_buf, socket_in_buf, socket_out_buf;
bool stdin_eof = false;
// Three distinct descriptors, not two: stdin (0) and stdout (1) are separate pipes
// (read-only and write-only respectively — never the same fd, even though they sit
// next to each other in a shell's mental model of "the process's stdio"), plus the
// bidirectional socket.
enum { kStdin, kStdout, kSock };
for (;;) {
pollfd fds[3] = {
{0, 0, 0},
{1, 0, 0},
{sock, 0, 0},
};
if (!stdin_eof) fds[kStdin].events |= POLLIN;
if (!stdout_buf.empty()) fds[kStdout].events |= POLLOUT;
fds[kSock].events |= POLLIN;
if (!socket_out_buf.empty()) fds[kSock].events |= POLLOUT;
// Nothing left to wait for: both directions exhausted.
if (fds[kStdin].events == 0 && fds[kStdout].events == 0 && fds[kSock].events == 0) break;
const int n = ::poll(fds, 3, -1);
if (n < 0) {
if (errno == EINTR) continue;
break;
}
if (fds[kStdout].revents & POLLOUT) {
if (!flush_some(1, stdout_buf)) break; // Firefox closed our stdout
}
if (fds[kSock].revents & POLLOUT) {
if (!flush_some(sock, socket_out_buf)) break;
}
if (fds[kStdin].revents & (POLLIN | POLLHUP)) {
if (read_available(0, stdin_buf)) stdin_eof = true;
if (!drain_stdin_frames(stdin_buf, socket_out_buf)) break;
}
if (fds[kSock].revents & (POLLIN | POLLHUP)) {
const bool socket_eof = read_available(sock, socket_in_buf);
if (!drain_socket_lines(socket_in_buf, stdout_buf)) break;
if (socket_eof) {
// The daemon is gone. Flush whatever we already turned into stdout
// frames, then stop — there is nothing left to relay either direction.
(void)flush_some(1, stdout_buf);
break;
}
}
if ((fds[kStdin].revents | fds[kStdout].revents | fds[kSock].revents) &
(POLLERR | POLLNVAL))
break;
// Firefox closed the pipe: nothing more will ever arrive on stdin, and once our
// own outbound backlog drains there is nothing left to send it either. Stop
// rather than idle forever relaying replies nobody reads.
if (stdin_eof && socket_out_buf.empty()) break;
}
::close(sock);
return 0;
}
+13
View File
@@ -0,0 +1,13 @@
# Integration test only velox-nmhost has no internal functions worth unit-testing in
# isolation (it's ~15 lines of byte-shuffling helpers around one poll() loop); what matters
# is the real binary's observable behaviour over real pipes and a real socket.
add_executable(velox_nmhost_pump_test pump_test.cpp)
target_compile_features(velox_nmhost_pump_test PRIVATE cxx_std_23)
target_compile_options(velox_nmhost_pump_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
target_compile_definitions(velox_nmhost_pump_test PRIVATE
VELOX_NMHOST_BIN="$<TARGET_FILE:velox-nmhost>")
add_dependencies(velox_nmhost_pump_test velox-nmhost)
add_test(NAME nmhost.pump COMMAND velox_nmhost_pump_test)
set_tests_properties(nmhost.pump PROPERTIES TIMEOUT 30)
+54
View File
@@ -0,0 +1,54 @@
#pragma once
// Minimal test harness: CHECK accumulates failures, TEST_MAIN reports and sets the exit
// code. Copied from daemon/tests/check.hpp rather than shared across a build-dependency —
// nmhost is deliberately dependency-free, tests included.
#include <cstdio>
#include <string>
#include <vector>
namespace veloxd_test {
inline std::vector<std::string>& failures() {
static std::vector<std::string> f;
return f;
}
inline int& checks() {
static int n = 0;
return n;
}
} // namespace veloxd_test
#define CHECK(cond) \
do { \
++::veloxd_test::checks(); \
if (!(cond)) { \
::veloxd_test::failures().push_back(std::string(__FILE__) + ":" + \
std::to_string(__LINE__) + ": " + #cond); \
} \
} while (0)
#define CHECK_EQ(a, b) \
do { \
++::veloxd_test::checks(); \
auto _va = (a); \
auto _vb = (b); \
if (!(_va == _vb)) { \
::veloxd_test::failures().push_back(std::string(__FILE__) + ":" + \
std::to_string(__LINE__) + ": " + #a + \
" == " + #b); \
} \
} while (0)
#define TEST_MAIN() \
int main() { \
run(); \
for (const auto& f : ::veloxd_test::failures()) std::printf("FAIL %s\n", f.c_str()); \
std::printf("%d/%d checks passed\n", \
::veloxd_test::checks() - \
static_cast<int>(::veloxd_test::failures().size()), \
::veloxd_test::checks()); \
return ::veloxd_test::failures().empty() ? 0 : 1; \
}
+148
View File
@@ -0,0 +1,148 @@
// Integration test for velox-nmhost: spawns the real binary, feeds it a framed stdin
// message, answers over a fake Unix socket standing in for veloxd, and checks what comes
// back out on stdout — plus that it exits promptly once stdin closes.
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <string>
#include "check.hpp"
namespace {
std::string make_temp_dir() {
char tmpl[] = "/tmp/velox-nmhost-test-XXXXXX";
const char* dir = ::mkdtemp(tmpl);
return dir ? dir : "/tmp";
}
// Firefox's own framing: 4-byte native-byte-order length, then that many bytes.
std::string frame(const std::string& payload) {
std::uint32_t len = static_cast<std::uint32_t>(payload.size());
std::string out(reinterpret_cast<char*>(&len), 4);
out += payload;
return out;
}
// Reads exactly one framed message off `fd`, blocking. Empty on EOF/short read.
std::string read_frame(int fd) {
std::uint32_t len = 0;
std::size_t got = 0;
while (got < 4) {
const ssize_t n = ::read(fd, reinterpret_cast<char*>(&len) + got, 4 - got);
if (n <= 0) return {};
got += static_cast<std::size_t>(n);
}
std::string payload(len, '\0');
got = 0;
while (got < len) {
const ssize_t n = ::read(fd, payload.data() + got, len - got);
if (n <= 0) return {};
got += static_cast<std::size_t>(n);
}
return payload;
}
bool write_all(int fd, const std::string& s) {
std::size_t off = 0;
while (off < s.size()) {
const ssize_t n = ::write(fd, s.data() + off, s.size() - off);
if (n <= 0) return false;
off += static_cast<std::size_t>(n);
}
return true;
}
} // namespace
void run() {
const std::string dir = make_temp_dir();
const std::string velox_dir = dir + "/velox";
CHECK(::mkdir(velox_dir.c_str(), 0700) == 0);
const std::string sock_path = velox_dir + "/velox.sock";
// A bare listening socket standing in for veloxd.
const int listen_fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
CHECK(listen_fd >= 0);
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
std::memcpy(addr.sun_path, sock_path.c_str(), sock_path.size());
CHECK(::bind(listen_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == 0);
CHECK(::listen(listen_fd, 1) == 0);
int child_stdin[2]; // [0] read (child), [1] write (parent)
int child_stdout[2]; // [0] read (parent), [1] write (child)
CHECK(::pipe(child_stdin) == 0);
CHECK(::pipe(child_stdout) == 0);
::setenv("XDG_RUNTIME_DIR", dir.c_str(), 1);
const pid_t pid = ::fork();
CHECK(pid >= 0);
if (pid == 0) {
::dup2(child_stdin[0], 0);
::dup2(child_stdout[1], 1);
::close(child_stdin[0]);
::close(child_stdin[1]);
::close(child_stdout[0]);
::close(child_stdout[1]);
::close(listen_fd);
::execl(VELOX_NMHOST_BIN, "velox-nmhost", nullptr);
::_exit(127);
}
::close(child_stdin[0]);
::close(child_stdout[1]);
// Accept nmhost's connection.
const int conn = ::accept(listen_fd, nullptr, nullptr);
CHECK(conn >= 0);
// stdin (framed) -> socket (NDJSON line).
CHECK(write_all(child_stdin[1], frame(R"({"hello":1})")));
char line[256] = {};
ssize_t n = ::read(conn, line, sizeof(line) - 1);
CHECK(n > 0);
CHECK_EQ(std::string(line, static_cast<std::size_t>(n)), std::string("{\"hello\":1}\n"));
// socket (NDJSON line) -> stdout (framed).
CHECK(write_all(conn, "{\"world\":2}\n"));
const std::string got = read_frame(child_stdout[0]);
CHECK_EQ(got, std::string(R"({"world":2})"));
// A second round trip on the same connection, to prove buffering across calls works
// (not just "the first message happens to line up with one read()").
CHECK(write_all(child_stdin[1], frame(R"({"again":3})")));
n = ::read(conn, line, sizeof(line) - 1);
CHECK(n > 0);
CHECK_EQ(std::string(line, static_cast<std::size_t>(n)), std::string("{\"again\":3}\n"));
// Firefox closes the pipe: nmhost must exit promptly rather than hang.
::close(child_stdin[1]);
int status = 0;
pid_t waited = -1;
for (int i = 0; i < 50 && waited != pid; ++i) {
waited = ::waitpid(pid, &status, WNOHANG);
if (waited == pid) break;
struct timespec ts{0, 20'000'000}; // 20ms
::nanosleep(&ts, nullptr);
}
CHECK_EQ(waited, pid);
if (waited == pid) CHECK(WIFEXITED(status));
::close(conn);
::close(child_stdout[0]);
::close(listen_fd);
::unlink(sock_path.c_str());
::rmdir(velox_dir.c_str());
::rmdir(dir.c_str());
}
TEST_MAIN()
+100
View File
@@ -1 +1,101 @@
Owner: lane PKG/QA. See ../docs/07-packaging.md. Owner: lane PKG/QA. See ../docs/07-packaging.md.
## `.deb` — local test build only
There is no PPA, no GPG signing, no Launchpad wiring here. This produces a `.deb` you
install with `dpkg -i` on the box you built it on (or an identical one), for testing —
not something to hand to a user or publish anywhere. See `docs/07-packaging.md`'s
package matrix for the eventual PPA plan.
```sh
sudo ./tools/bootstrap.sh --packaging # debhelper, dpkg-dev, lintian, devscripts, fakeroot
dpkg-buildpackage -us -uc -b # from the repo root; -b = binary only, no signing
sudo dpkg -i ../velox_0.1.0-1_amd64.deb
lintian ../velox_0.1.0-1_amd64.deb
```
Uninstall: `sudo dpkg -r velox` (or `sudo dpkg -P velox` to purge — see below for what
purge does and does not do here).
### Known limitations of this build — read before relying on any of these
- **`libveloxcore` is static, not `.so.1`.** The install layout in `docs/07-packaging.md`
originally called for a shared, versioned `libveloxcore.so.1`; the user decided against
building one for now (no consumer needs it as a shared object yet, and it would need a
`SOVERSION` added to CORE's own `core/CMakeLists.txt`). The doc was corrected in the
same commit as this packaging work — it no longer describes something unshipped.
- **Placeholder app icon.** `gui/resources/icons/` (GUI's lane) is empty — no real icon
art exists anywhere in the repo yet. `packaging/icons/hicolor/*/apps/velox.png` is a
plain generated placeholder (flat color, a download-arrow glyph), good enough to
install correctly and pass `desktop-file-validate`/lintian's icon checks, and nothing
more. **Replace it before any real release** — and do the icon-set licence audit
`docs/07-packaging.md`'s release checklist asks for once real art exists (this
placeholder was drawn from scratch for this package, so it has no licence question of
its own, but it is not real Velox branding either).
- **No license has been chosen for Velox's own source yet.** There is no top-level
`LICENSE` file in the repo. `debian/copyright` says so plainly (`License:
Unlicensed-TODO`) rather than inventing one — that is a real project decision, above
any one lane. Expect `lintian` to flag this package over it; that is lintian doing its
job, not a bug in this packaging, and it stays a known exception to "lintian clean"
until the project adopts a license. The release checklist's "Licence audit" item (Qt,
libcurl, SQLite, ffmpeg, icon set) is separate and still open regardless.
- **Pairing has no real approval UI in this build.** `libdbus-1-dev` is not in this
build's `Build-Depends` (D1's GUI-dialog/desktop-notification approver needs it and
isn't wired up yet — `daemon/src/main.cpp` still uses `EnvAutoApprover`
unconditionally). That approver auto-**rejects** every pairing request unless
`VELOX_PAIR_AUTO=1` is set in `veloxd`'s environment — there is no prompt, no
accept/decline, it is all-or-nothing. `postinst` prints this prominently after
install; it is repeated here so it isn't missed by anyone reading only one of the two.
**Do not set `VELOX_PAIR_AUTO=1` on a machine or account where auto-accepting every
pairing request is unacceptable.**
- **Socket activation is real.** `velox.service` and `velox.socket` both ship, verbatim
from DAEMON's `packaging/systemd/README.md``veloxd` receives the pre-bound fd via
`sd_listen_fds()` (`daemon/src/rpc/systemd_activation.cpp`) rather than binding its own
socket. `debian/rules` does **not** disable `dh_installsystemduser` (only the
system-level `dh_installsystemd`, since these are `--user` units); debhelper finds both
units under `lib/systemd/user/` in the install tree and enables the pair together
(`Also=velox.socket` in `velox.service`'s `[Install]`).
- **The systemd `--user` service starts enabled for your *next* login, not this one.**
debhelper's `dh_installsystemduser` (verified in the built package's `postinst`) marks
`velox.service` enabled automatically — that takes effect the next time each user's
`systemd --user` instance starts. It cannot reach an *already-running* session from a
root maintainer script, so for the session you're in right now, `postinst` prints:
```sh
systemctl --user daemon-reload
systemctl --user start velox.service
```
- **`dpkg -P velox` (purge) does not delete user data.** The uninstall test this package
is built to pass is "manifests, units and sockets removed; user data untouched *unless
purged*" — this build is deliberately conservative and leaves `$XDG_DATA_HOME/velox`
(normally `~/.local/share/velox`, the task database) alone on **both** `remove` and
`purge`, for every account. A root maintainer script deleting per-user data across
every account on the system is exactly the kind of destructive, hard-to-test operation
that deserves its own design pass, not a first cut bolted on here. `postrm` only ever
undoes what `postinst` did: the per-user native-messaging manifest it copied in.
### The uninstall test, concretely
What `docs/07-packaging.md`'s release checklist asks for, and what this build actually
does:
| Removed on `dpkg -r`/`-P`? | |
|---|---|
| `/usr/bin/{veloxd,velox,velox-gui}`, `/usr/libexec/velox/velox-nmhost` | yes — dpkg itself, ordinary package files |
| `/usr/lib/systemd/user/{velox.service,velox.socket}` | yes — same |
| `/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json` | yes — same |
| Per-user `~/.mozilla/native-messaging-hosts/com.velox.host.json` | yes — `postrm`, both `remove` and `purge` |
| `$XDG_DATA_HOME/velox` (task DB, settings) | **no, on either `remove` or `purge`** — see above |
### Native messaging
`velox-nmhost` (DAEMON's `nmhost/`) and its manifest (DAEMON's
`packaging/nativehost/com.velox.host.json`) now ship in this package — the top-level
`CMakeLists.txt`'s install rules are guarded on `if(TARGET velox-nmhost)`, the same way its
`add_subdirectory()` calls are, so this lit up automatically the day that lane merged, no
packaging change needed on this end.
View File
+12
View File
@@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Version=1.0
Name=Velox
GenericName=Download Manager
Comment=IDM-class download manager
Exec=velox-gui
Icon=velox
Terminal=false
Categories=Network;FileTransfer;
Keywords=download;downloader;idm;resume;segmented;
StartupNotify=true
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+20
View File
@@ -0,0 +1,20 @@
.TH VELOX\-GUI 1 "2026-09-12" "velox 0.1.0" "Velox Download Manager"
.SH NAME
velox-gui \- Velox download manager, graphical client
.SH SYNOPSIS
.B velox-gui
.SH DESCRIPTION
.B velox-gui
is the Qt graphical client for
.BR veloxd (8),
the Velox download manager daemon. It takes no command-line arguments and connects
to the daemon over its local Unix socket.
.B veloxd
must already be running \(em see
.BR veloxd (8)
for starting it as a
.B systemd --user
service.
.SH SEE ALSO
.BR velox (1),
.BR veloxd (8)
+35
View File
@@ -0,0 +1,35 @@
.TH VELOXD 8 "2026-09-12" "velox 0.1.0" "Velox Download Manager"
.SH NAME
veloxd \- Velox download manager daemon
.SH SYNOPSIS
.B veloxd
.SH DESCRIPTION
.B veloxd
is the background daemon behind the Velox download manager. It manages downloads,
segmented transfers and resume state in a local SQLite database, and exposes a
JSON-RPC API over a local Unix socket (for
.BR velox (1)
and
.BR velox-gui (1))
and a loopback WebSocket (for the Firefox extension).
.PP
It takes no command-line arguments. It is normally started as a
.B systemd --user
service:
.RS
.nf
systemctl --user enable --now velox.service
.fi
.RE
.SH FILES
.TP
.I $XDG_RUNTIME_DIR/velox/velox.sock
The Unix socket clients connect to.
.TP
.I $XDG_DATA_HOME/velox/velox.db
The task and settings database (default
.IR ~/.local/share/velox/velox.db ).
.SH SEE ALSO
.BR velox (1),
.BR velox-gui (1),
.BR systemctl (1)
View File
+97
View File
@@ -0,0 +1,97 @@
# packaging/nativehost — the Firefox native-messaging manifest
Owner: lane DAEMON (`packaging/nativehost/**` is explicitly DAEMON's per `CLAUDE.md`'s
lane table, unlike the rest of `packaging/`, which is PKG/QA's). This directory holds the
manifest `velox-nmhost` (built from `nmhost/`) is registered under, and this file is the
one place that says exactly which on-disk locations need it and why — the
"install manifests to all four locations" line in `docs/agents/AGENT-DAEMON.md`'s build
order predates `docs/adr/0003-native-messaging-under-snap.md`, which cut that down to
three *real* ones. Read the ADR before touching this list; it is the empirical spike, not
a guess.
## The manifest
`com.velox.host.json` in this directory is the template:
```json
{
"name": "com.velox.host",
"description": "Velox download manager — native messaging bridge to veloxd",
"path": "/usr/libexec/velox/velox-nmhost",
"type": "stdio",
"allowed_extensions": ["velox@velox.download"]
}
```
- `name` is what the extension calls `browser.runtime.connectNative("com.velox.host")`
with (`extension/src/background/transport/native.ts`'s `HOST_NAME`) — do not rename one
without the other.
- `allowed_extensions` must match `extension/manifest.json`'s
`browser_specific_settings.gecko.id` exactly (`[email protected]`). A mismatch here is
a silent failure: Firefox reports "no such native application" as if the manifest didn't
exist at all, which reads exactly like a missing-file bug and is easy to chase in the
wrong place.
- `path` is `/usr/libexec/velox/velox-nmhost` — the `.deb`'s install layout
(`docs/07-packaging.md`). **Every other packaging format needs a different `path`**
the binary doesn't live at that absolute path inside a Flatpak sandbox or an AppImage
mount, so whatever installs the manifest for those formats must rewrite this field to
wherever it actually put the binary, not ship this file verbatim. That substitution is
the packaging step's job, not this template's.
## Where it has to be written, and why
Per ADR 0003 (spike S1, run against the real snap Firefox on the target machine — not a
guess about how confinement "should" work):
| Location | Covers | Why |
|---|---|---|
| `~/.mozilla/native-messaging-hosts/com.velox.host.json` | **deb/tarball Firefox, and snap Firefox** | The one location snap Firefox 154 actually reads. Firefox's snap launches native-messaging hosts *outside* the sandbox with the user's real `$HOME` — so this is not "the deb location that happens to also work"; it is *the* snap location, full stop. `~/snap/firefox/common/.mozilla/native-messaging-hosts/` (the intuitive "inside the snap" path) is **not** read by Firefox 154 snap — confirmed empirically, not inferred. |
| `/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json` | **deb/tarball Firefox only** | System-wide, so it covers every user on the box for a real (non-snap) Firefox install — but confirmed **not** read by snap Firefox. Do not treat this path as "covers snap too"; that was the wrong assumption `docs/05` §4 corrected in the same commit as the ADR. |
| `~/.var/app/org.mozilla.firefox/.mozilla/native-messaging-hosts/com.velox.host.json` | **Flatpak Firefox** | Flatpak's own sandboxed home. Untested on this machine (Firefox here is the snap, not flatpak) — carried over from `docs/05` §4's original four-location list, which this table otherwise supersedes. Verify before relying on it in a release checklist. |
That is three real locations, not four — the fourth
(`~/snap/firefox/common/.mozilla/native-messaging-hosts/`) was the pre-ADR guess the spike
disproved. If `docs/agents/AGENT-DAEMON.md`'s build order still says "four locations" when
you read this, it is stale; this table is the current source of truth alongside the ADR
itself.
## What this means for `postinst` (PKG/QA's file, not this one)
This directory ships the manifest template and documents the target paths; it does not
install anything itself (`packaging/**` outside this one directory is PKG/QA's — see
`CLAUDE.md`'s lane table — and `postinst` specifically is a `.deb`-packaging concern this
lane doesn't own the file for). For whoever writes it:
- **The two `~/`-relative locations are per-user.** `postinst` runs as root, once, at
install time — it does not run once per user session. It needs either a real-user
enumeration at install time (every UID with a home directory and no login shell of
`/usr/sbin/nologin`-style exclusions, roughly what `deluser --remove-home` scripts already
have to reason about) or a first-run hook that runs as the logged-in user (a systemd user
unit's `ExecStartPre`, or the GUI's own first-run wizard) and writes its own manifest the
first time it starts. `docs/adr/0003`'s own follow-up section flagged this as open; it
still is.
- **`/usr/lib/mozilla/native-messaging-hosts/` is the only one of the three that's a plain
root-owned, install-time write** — no per-user enumeration needed for that one.
- **Detect snap Firefox and say so.** `docs/07-packaging.md` already commits to this
("detects whether Firefox is a snap... prints... a one-line note that the extension will
pair over loopback") — the detection matters here specifically because if Firefox turns
out to be neither deb/tarball nor a snap this spike covers (a genuinely unknown or future
packaging of Firefox), silently trusting `NativeTransport` to work is exactly the failure
mode ADR 0003 exists to prevent. `WebSocketTransport` is the guaranteed fallback either
way (ADR 0003's own decision) — nothing breaks if the manifest doesn't land correctly, it
just means the extension pairs over loopback instead of the (opportunistic, not
required) native path.
- **`docs/07-packaging.md`'s own install layout line currently lists only the
system-wide `/usr/lib/mozilla/...` path.** That line is correct as far as it goes (it *is*
one of the three locations, and the only pure root-owned one) but reads as if it were the
whole story for native messaging; it predates this file and the ADR. Worth a line
pointing here so the two documents don't quietly disagree — PKG/QA's call, not edited
here since `docs/07` is PKG/QA's own file.
## The binary
`nmhost/` builds `velox-nmhost` — see that directory's own `src/main.cpp` for what it does
(a byte-level pump, no protocol logic) and `docs/05-extension-spec.md` §4 for the two
transports it sits behind. It is deliberately dependency-free (not linked against any
`veloxd_*` library) so wherever a packaging format's sandbox puts it, it has nothing else
to go looking for at runtime.
+7
View File
@@ -0,0 +1,7 @@
{
"name": "com.velox.host",
"description": "Velox download manager — native messaging bridge to veloxd",
"path": "/usr/libexec/velox/velox-nmhost",
"type": "stdio",
"allowed_extensions": ["[email protected]"]
}
+65
View File
@@ -0,0 +1,65 @@
# packaging/systemd — the user unit pair for veloxd
Provided by lane DAEMON (`daemon/docs/AGENT-DAEMON.md` build step 7: "systemd user units
`velox.service` + `velox.socket` for socket activation"), for PKG/QA to install per
`docs/07-packaging.md`'s layout:
```
/usr/lib/systemd/user/velox.service
/usr/lib/systemd/user/velox.socket
```
`packaging/` outside `nativehost/` is PKG/QA's per `CLAUDE.md`'s lane table; these two
files are here because they are inputs to that packaging step, not a claim on the rest of
the directory — same relationship `packaging/nativehost/` already has.
## Why both files, and what `RuntimeDirectory=` is doing
`velox.socket` binds `$XDG_RUNTIME_DIR/velox/velox.sock` **before `veloxd` ever runs** and
hands the daemon the already-listening fd at startup (`daemon/src/rpc/systemd_activation.cpp`
implements the receiving half — `LISTEN_PID`/`LISTEN_FDS`, fd 3 — without a `libsystemd`
link). Two things this buys over the daemon binding its own socket on every start:
- **No window where a client gets `ECONNREFUSED`.** The socket exists and queues
connections from the moment `velox.socket` is active, not from whenever `veloxd`
finishes starting up — this is the actual point of socket activation, not just "start
on demand."
- **Cold-boot ordering is free.** Nothing has to wait for `veloxd` to be ready before the
GUI, the CLI, or a native-messaging host can attempt a connection; the kernel queues it.
`RuntimeDirectory=velox` on the socket unit creates `%t/velox` (mode 0700) before the
`ListenStream=` bind — without it, binding fails outright the first time (nothing has
created the parent directory yet). `veloxd` itself creates that same directory
(`ensure_private_dir` in `runtime_dir.cpp`) for the case where it's started directly,
outside systemd (`./veloxd` in a terminal, still supported and how most of this daemon's
own testing runs) — the two paths converge on the same directory with the same mode
either way.
`UdsServer::start()` (`daemon/src/rpc/uds_server.cpp`) checks for the activated fd first
and, if present, skips create/bind/chmod/listen entirely — the socket file's lifecycle then
belongs to the unit (including `RemoveOnStop=yes` on stop), not to the daemon. Falls back
to binding its own socket exactly as before when not socket-activated (a manual run, or a
distro that ships the daemon without the unit files).
## What was deliberately left out
`velox.service` does **not** set `ProtectSystem=`, `ProtectHome=`, or `ReadWritePaths=`.
`saveTo.allowedRoots` is user-configurable to anywhere on the filesystem — an external
drive, a second mount, anywhere `fs/safepath.hpp`'s own canonicalize-and-check accepts —
not a fixed set of directories a unit file could enumerate ahead of time. A filesystem-level
sandbox here would turn a legitimately-configured save location into an opaque
`EROFS`/`EACCES` the daemon can't explain, in place of its own clear `-32011` — worse than
no sandbox, specifically for a download manager. `NoNewPrivileges=yes` is kept: it has no
such trade-off.
## Verifying socket activation without a real install
`systemd-analyze verify --user velox.service velox.socket` checks unit-file syntax (it
will complain that `/usr/bin/veloxd` and the `velox(1)` man page don't exist on a dev
box that hasn't installed the package — expected, not a unit bug). To exercise the actual
activation handshake without installing anything: bind a Unix socket, `dup2` it onto fd 3,
fork, set `LISTEN_PID=<child pid>` and `LISTEN_FDS=1` in the child's environment, clear
`FD_CLOEXEC` on fd 3, and `execve` `veloxd` — a real `session.hello` round-trips over that
fd with no `bind()`/`listen()` call ever happening inside the daemon for that run. This is
exactly what `velox.socket`'s `Requires=`/`ExecStart` sequence does in production; systemd
supplies the fd, `veloxd` doesn't know the difference.
+38
View File
@@ -0,0 +1,38 @@
[Unit]
Description=Velox download manager daemon
Documentation=man:velox(1)
# Socket activation (velox.socket) means this unit does not need to be enabled or started
# directly for the RPC transport to come up on demand — the first connection attempt after
# boot starts veloxd with the listening socket already bound (see velox.socket's own
# comment). Requires=/After= still matter for a manual `systemctl --user start velox`.
Requires=velox.socket
After=velox.socket
# Never more than one real instance for this user regardless of how it was started — the
# abstract-socket single-instance lock (main.cpp, keyed off the resolved runtime dir) is
# the actual enforcement; this just keeps systemd itself from racing two starts.
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=simple
ExecStart=/usr/bin/veloxd
# main.cpp's SIGTERM handler stops the event loop and falls through to a clean shutdown
# (flushes buffers, closes the store, releases the single-instance lock) — the default
# KillSignal=SIGTERM and TimeoutStopSec are already the right shape for that; no
# ExecStop/KillMode override needed.
Restart=on-failure
RestartSec=2
# Hardening deliberately stops here, not at ProtectSystem=/ProtectHome=/ReadWritePaths=:
# saveTo.allowedRoots is user-configurable to anywhere (an external drive, a second
# mount — fs/safepath.hpp is the daemon's own validation boundary, not a fixed set of
# directories a unit file could enumerate up front). A filesystem-level sandbox here would
# silently turn a legitimately-configured save location into an opaque EROFS/EACCES the
# daemon can't explain, instead of its own clear -32011 — worse than no sandbox, for a
# download manager specifically. NoNewPrivileges is free of that trade-off.
NoNewPrivileges=yes
[Install]
WantedBy=default.target
Also=velox.socket

Some files were not shown because too many files have changed in this diff Show More