Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ cmake --build build
- `echoflow.service`——语音识别后台守护进程
- `echoflow-ui.service`——托盘图标和语音胶囊提示

两个服务在图形会话就绪后启动,注销时停止,重新登录后自动启动。
用户安装脚本会迁移旧版本的自启动链接;deb 升级会迁移系统级链接,
但不会覆盖 `~/.config/systemd/user/` 下已有的用户安装版服务。
若曾从源码安装,再安装 deb,请先运行 `./uninstall-user.sh` 移除用户版
(保留模型和配置),避免用户版覆盖系统包。

本地安装会同时重载 Fcitx,使用户目录中的 addon 和对应的绝对模块路径
立即生效。使用 `--no-start` 时不会修改当前 Fcitx 会话,稍后需手动执行
`fcitx5 -rd`。
Expand Down
17 changes: 17 additions & 0 deletions debian/postinst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2026 Hualet Wang
# SPDX-License-Identifier: GPL-3.0-or-later
set -e

# Migrate the old default.target links before debhelper records the new ones.
# Preserve services disabled by the administrator.
if [ "$1" = configure ] && [ -z "${DPKG_ROOT:-}" ]; then
for unit in echoflow.service echoflow-ui.service; do
if deb-systemd-helper --quiet --user debian-installed "$unit" &&
deb-systemd-helper --quiet --user was-enabled "$unit"; then
deb-systemd-helper --user reenable "$unit" >/dev/null || true
fi
done
fi

#DEBHELPER#
3 changes: 2 additions & 1 deletion install-user.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ install -m 0644 "$BUILD_DIR/systemd/user/echoflow.service" "$SYSTEMD_USER_DIR/ec
install -m 0644 "$BUILD_DIR/systemd/user/echoflow-ui.service" "$SYSTEMD_USER_DIR/echoflow-ui.service"

systemctl --user daemon-reload
systemctl --user enable echoflow.service echoflow-ui.service
# Replace links from older installations that started at default.target.
systemctl --user reenable echoflow.service echoflow-ui.service
if [[ "$START_SERVICES" == "1" ]]; then
systemctl --user restart echoflow.service echoflow-ui.service
if command -v fcitx5 >/dev/null 2>&1; then
Expand Down
4 changes: 3 additions & 1 deletion systemd/user/echoflow-ui.service.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

[Unit]
Description=EchoFlow tooltip UI host
Requisite=graphical-session.target
After=graphical-session.target
PartOf=graphical-session.target

[Service]
Type=simple
ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/echoflow-ui
Restart=on-failure

[Install]
WantedBy=default.target
WantedBy=graphical-session.target
4 changes: 3 additions & 1 deletion systemd/user/echoflow.service.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

[Unit]
Description=EchoFlow offline voice input service
Requisite=graphical-session.target
After=graphical-session.target
PartOf=graphical-session.target

[Service]
Type=simple
Expand All @@ -13,4 +15,4 @@ ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/echoflow-service
Restart=on-failure

[Install]
WantedBy=default.target
WantedBy=graphical-session.target
11 changes: 6 additions & 5 deletions tests/spec/run_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ assert_contains "$ROOT/install-user.sh" 'cmake -S "$ROOT_DIR"' "install-user.sh
assert_contains "$ROOT/install-user.sh" 'cmake --install "$BUILD_DIR"' "install-user.sh installs root build"
assert_contains "$ROOT/install-user.sh" '$BUILD_DIR/systemd/user/echoflow.service' "install-user.sh installs configured service unit"
assert_contains "$ROOT/install-user.sh" '$BUILD_DIR/systemd/user/echoflow-ui.service' "install-user.sh installs configured UI service unit"
assert_contains "$ROOT/install-user.sh" 'systemctl --user enable echoflow.service echoflow-ui.service' "install-user.sh always enables user services"
assert_contains "$ROOT/install-user.sh" 'systemctl --user reenable echoflow.service echoflow-ui.service' "install-user.sh migrates user service links"
assert_contains "$ROOT/install-user.sh" 'systemctl --user restart echoflow.service echoflow-ui.service' "started installs restart both user services"
assert_absent "$ROOT/install-user.sh" 'systemctl --user enable --now' "install-user.sh does not leave running services on old binaries"
assert_before "$ROOT/install-user.sh" 'if [[ "$START_SERVICES" == "1" ]]' 'systemctl --user restart echoflow.service echoflow-ui.service' "service restart is guarded by START_SERVICES"
assert_before "$ROOT/install-user.sh" 'systemctl --user enable echoflow.service echoflow-ui.service' 'if [[ "$START_SERVICES" == "1" ]]' "no-start still enables services before skipping restart"
assert_before "$ROOT/install-user.sh" 'systemctl --user reenable echoflow.service echoflow-ui.service' 'if [[ "$START_SERVICES" == "1" ]]' "no-start still enables services before skipping restart"

assert_absent "$ROOT/CMakeLists.txt" "qwen-asr-runtime" "top-level no longer builds qwen-asr-runtime"
assert_contains "$ROOT/CMakeLists.txt" 'set(ECHOFLOW_CPU_TARGET "native" CACHE STRING' "CMake exposes the CPU target"
Expand Down Expand Up @@ -338,11 +338,12 @@ fi
assert_contains "$ROOT/debian/control" "Package: echoflow" "debian control defines echoflow package"
assert_contains "$ROOT/debian/control" "debhelper-compat (= 13)" "debian control uses debhelper compat 13"
assert_contains "$ROOT/debian/control" "desktop-file-utils" "debian build dependencies provide desktop-file-validate"
if [[ ! -e "$ROOT/debian/postinst" ]]; then
echo "ok - Debian uses debhelper-generated user-service maintainer scripts"
assert_contains "$ROOT/debian/postinst" '#DEBHELPER#' "Debian retains generated maintainer scripts"
assert_absent "$ROOT/debian/postinst" 'systemctl' "Debian migration does not restart user sessions"
if bash "$ROOT/tests/spec/test_systemd_upgrade.sh"; then
pass=$((pass + 1))
else
echo "FAIL - manual Debian postinst would duplicate debhelper user-service restart"
echo "FAIL - Debian session autostart migration"
fail=$((fail + 1))
fi
assert_contains "$ROOT/debian/control" "libdtk6widget-dev (>= 6.7)" "debian control keeps DTK widget dependency at minor version"
Expand Down
6 changes: 3 additions & 3 deletions tests/spec/test_onboarding_install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@ DEFAULT_HOME="$TEST_ROOT/default-home"
: > "$TEST_LOG"
default_output="$(run_install "$DEFAULT_HOME")"
assert_logged "systemctl --user daemon-reload"
assert_logged "systemctl --user enable echoflow.service echoflow-ui.service"
assert_logged "systemctl --user reenable echoflow.service echoflow-ui.service"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · User link migration goes unverified

When the fake systemctl is used for the user-install migration check, test_onboarding_install.sh accepts the logged reenable command at line 98 as migration success; its fake systemctl at lines 41–48 only logs arguments and always exits zero, so the test passes with the old default.target link unchanged.

Assert the resulting link state using an isolated systemd-compatible helper, or make the fake model reenable and verify that it replaces the old link.

Prompt for AI agents
In `tests/spec/test_onboarding_install.sh` at line 98:

**User link migration goes unverified**

When the fake `systemctl` is used for the user-install migration check, `test_onboarding_install.sh` accepts the logged `reenable` command at line 98 as migration success; its fake `systemctl` at lines 41–48 only logs arguments and always exits zero, so the test passes with the old `default.target` link unchanged.

Assert the resulting link state using an isolated systemd-compatible helper, or make the fake model `reenable` and verify that it replaces the old link.

assert_logged "systemctl --user restart echoflow.service echoflow-ui.service"
assert_logged "fcitx5 -rd"
assert_ordered \
"systemctl --user daemon-reload" \
"systemctl --user enable echoflow.service echoflow-ui.service" \
"systemctl --user reenable echoflow.service echoflow-ui.service" \
"systemctl --user restart echoflow.service echoflow-ui.service" \
"fcitx5 -rd"
grep -qF 'EchoFlow installed.' <<< "$default_output" || fail "default install did not report success"
Expand All @@ -109,7 +109,7 @@ NO_START_HOME="$TEST_ROOT/no-start-home"
: > "$TEST_LOG"
no_start_output="$(run_install "$NO_START_HOME" --no-start)"
assert_logged "systemctl --user daemon-reload"
assert_logged "systemctl --user enable echoflow.service echoflow-ui.service"
assert_logged "systemctl --user reenable echoflow.service echoflow-ui.service"
assert_not_logged "systemctl --user restart"
assert_not_logged "fcitx5"
grep -qF 'Start services later:' <<< "$no_start_output" || fail "no-start guidance missing"
Expand Down
75 changes: 75 additions & 0 deletions tests/spec/test_systemd_upgrade.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Hualet Wang
# SPDX-License-Identifier: GPL-3.0-or-later
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/echoflow-systemd-spec.XXXXXX")"
trap 'rm -rf "$TEST_ROOT"' EXIT
HELPER="$(command -v deb-systemd-helper || true)"
if [[ -z "$HELPER" ]]; then
echo 'SKIP - deb-systemd-helper is not installed'
exit 0
fi

fail() { echo "FAIL - $*" >&2; exit 1; }

# Exercise the real Debian helper without touching the host service state.
mkdir -p "$TEST_ROOT/bin"
export HELPER
export DPKG_MAINTSCRIPT_PACKAGE=echoflow
cat > "$TEST_ROOT/bin/deb-systemd-helper" <<'EOF'
#!/bin/sh
DPKG_ROOT="$TEST_PACKAGE_ROOT" exec "$HELPER" "$@"
EOF
chmod +x "$TEST_ROOT/bin/deb-systemd-helper"

for scenario in enabled disabled fresh preset-disabled; do
export TEST_PACKAGE_ROOT="$TEST_ROOT/$scenario"
units="$TEST_PACKAGE_ROOT/usr/lib/systemd/user"
mkdir -p "$units" "$TEST_PACKAGE_ROOT/usr/bin" "$TEST_PACKAGE_ROOT/etc/systemd/user-preset"
printf '[Unit]\nDescription=Test user manager\n' > "$units/default.target"
printf '[Unit]\nDescription=Test graphical session\n' > "$units/graphical-session.target"
ln -s "$(command -v systemctl)" "$TEST_PACKAGE_ROOT/usr/bin/systemctl"
if [[ "$scenario" == preset-disabled ]]; then
printf 'disable echoflow*\n' > "$TEST_PACKAGE_ROOT/etc/systemd/user-preset/00-test.preset"
fi
for unit in echoflow.service echoflow-ui.service; do
sed -e 's|@CMAKE_INSTALL_FULL_BINDIR@|/usr/bin|g' \
-e 's/WantedBy=graphical-session.target/WantedBy=default.target/' \
"$ROOT/systemd/user/$unit.in" > "$units/$unit"
if [[ "$scenario" == enabled || "$scenario" == disabled ]]; then
DPKG_ROOT="$TEST_PACKAGE_ROOT" "$HELPER" --user enable "$unit"
if [[ "$scenario" == disabled ]]; then
DPKG_ROOT="$TEST_PACKAGE_ROOT" "$HELPER" --user disable "$unit"
fi
fi
sed 's|@CMAKE_INSTALL_FULL_BINDIR@|/usr/bin|g' \
"$ROOT/systemd/user/$unit.in" > "$units/$unit"
done

# The maintscript must run before debhelper updates its recorded link state.
env -u DPKG_ROOT PATH="$TEST_ROOT/bin:$PATH" sh "$ROOT/debian/postinst" configure
for unit in echoflow.service echoflow-ui.service; do
if [[ "$scenario" == fresh || "$scenario" == preset-disabled ]]; then
if DPKG_ROOT="$TEST_PACKAGE_ROOT" "$HELPER" --quiet --user debian-installed "$unit"; then
fail "migration bypassed first-install preset handling: $unit"
fi
fi
# Complete the normal debhelper enable/update-state sequence.
if DPKG_ROOT="$TEST_PACKAGE_ROOT" "$HELPER" --quiet --user was-enabled "$unit"; then
DPKG_ROOT="$TEST_PACKAGE_ROOT" "$HELPER" --user enable "$unit"
else
DPKG_ROOT="$TEST_PACKAGE_ROOT" "$HELPER" --user update-state "$unit"
fi
old="$TEST_PACKAGE_ROOT/etc/systemd/user/default.target.wants/$unit"
new="$TEST_PACKAGE_ROOT/etc/systemd/user/graphical-session.target.wants/$unit"
[[ ! -L "$old" ]] || fail "$scenario retained boot-only link: $unit"
if [[ "$scenario" == disabled || "$scenario" == preset-disabled ]]; then
[[ ! -L "$new" ]] || fail "upgrade enabled a disabled service: $unit"
else
[[ -L "$new" ]] || fail "$scenario missing session autostart: $unit"
fi
done
done
echo 'ok - Debian session autostart migration preserves disabled services and presets'
Loading