Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0f22f808f | ||
|
|
111229686c | ||
|
|
a5551db1c3 | ||
|
|
83b92d118b | ||
|
|
78f2b22c8c | ||
|
|
77bdb0c427 | ||
|
|
d60cf94905 | ||
|
|
0cd5bf6882 | ||
|
|
31eadffce5 | ||
|
|
5b42e1f0e5 | ||
|
|
e8af9b46b1 | ||
|
|
8abfc6a428 | ||
|
|
936136db06 | ||
|
|
6d7c4f7bc4 | ||
|
|
9487202ea8 | ||
|
|
9624e685af | ||
|
|
4e87730ce9 | ||
|
|
facd851824 | ||
|
|
eb72aa522c |
@@ -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-*/
|
||||||
|
|||||||
@@ -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.**
|
||||||
|
|||||||
@@ -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)
|
||||||
find_package(PkgConfig REQUIRED)
|
# libsecret / Secret Service is Linux-only (docs/adr/0020-cross-platform-strategy.md,
|
||||||
pkg_check_modules(LIBSECRET REQUIRED IMPORTED_TARGET libsecret-1)
|
# 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)
|
||||||
|
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)
|
||||||
find_package(Qt6 6.6 REQUIRED COMPONENTS Widgets Svg Network LinguistTools)
|
# 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)
|
||||||
|
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()
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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()
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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);
|
|
||||||
return sys_error("ftruncate " + p, e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
::close(fd);
|
::close(fd);
|
||||||
return sys_error("posix_fallocate " + p, rc);
|
return std::move(r).error();
|
||||||
}
|
}
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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. |
|
||||||
|
|||||||
@@ -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,57 +108,39 @@ 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) {
|
const auto rows = scheduler.progress_snapshot();
|
||||||
itimerspec spec{};
|
if (rows.empty()) return;
|
||||||
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();
|
|
||||||
if (rows.empty()) return;
|
|
||||||
|
|
||||||
nlohmann::json tasks_json = nlohmann::json::array();
|
nlohmann::json tasks_json = nlohmann::json::array();
|
||||||
for (const auto& r : rows) {
|
for (const auto& r : rows) {
|
||||||
nlohmann::json t{{"taskId", r.task_id},
|
nlohmann::json t{{"taskId", r.task_id},
|
||||||
{"downloadedBytes", r.downloaded_bytes},
|
{"downloadedBytes", r.downloaded_bytes},
|
||||||
{"speedBps", r.speed_bps}};
|
{"speedBps", r.speed_bps}};
|
||||||
t["etaSeconds"] = r.eta_seconds ? nlohmann::json(*r.eta_seconds) : nlohmann::json(nullptr);
|
t["etaSeconds"] = r.eta_seconds ? nlohmann::json(*r.eta_seconds) : nlohmann::json(nullptr);
|
||||||
if (!r.segments.empty()) {
|
if (!r.segments.empty()) {
|
||||||
nlohmann::json segs = nlohmann::json::array();
|
nlohmann::json segs = nlohmann::json::array();
|
||||||
for (const auto& s : r.segments)
|
for (const auto& s : r.segments)
|
||||||
segs.push_back({{"index", s.index},
|
segs.push_back({{"index", s.index},
|
||||||
{"downloadedBytes", s.downloaded_bytes},
|
{"downloadedBytes", s.downloaded_bytes},
|
||||||
{"speedBps", s.speed_bps}});
|
{"speedBps", s.speed_bps}});
|
||||||
t["segments"] = std::move(segs);
|
t["segments"] = std::move(segs);
|
||||||
}
|
|
||||||
tasks_json.push_back(std::move(t));
|
|
||||||
}
|
}
|
||||||
const nlohmann::json params{{"tasks", std::move(tasks_json)},
|
tasks_json.push_back(std::move(t));
|
||||||
{"at", velox::daemon::now_iso()}};
|
}
|
||||||
hub.publish(velox::proto::Event::TaskProgress,
|
const nlohmann::json params{{"tasks", std::move(tasks_json)},
|
||||||
velox::proto::make_notification(velox::proto::Event::TaskProgress, params));
|
{"at", velox::daemon::now_iso()}};
|
||||||
});
|
hub.publish(velox::proto::Event::TaskProgress,
|
||||||
}
|
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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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:
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.0 (native)
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 1–2)
|
||||||
|
- 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.
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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_);
|
||||||
|
|||||||
@@ -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 ¶ms);
|
void onSettingsChanged(const QJsonObject ¶ms);
|
||||||
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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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; \
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 562 B |
|
After Width: | Height: | Size: 804 B |
|
After Width: | Height: | Size: 900 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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.
|
||||||
@@ -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]"]
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||