Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .trellis/spec/backend/directory-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def main() -> None:
新增实现时按职责拆分模块:

- `cli.py`:Typer 应用、命令参数和用户输出。
- `service.py`:`sc.exe` 调用、服务安装、卸载、启动、停止、重启。
- `service.py`:`nssm.exe` 调用、服务安装、卸载、启动、停止、重启。
- `state.py`:`state.json` 读写、profile 名称校验、active 状态维护。
- `profile.py`:profile URL 下载、本地 profile 文件写入。
- `errors.py`:面向 CLI 的异常类型。
Expand Down
12 changes: 6 additions & 6 deletions .trellis/spec/backend/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@ CLI 必须让失败清晰暴露。不要为了让命令“看起来成功”添
- 找不到 `sing-box.exe`、路径不存在或路径不是文件时,命令失败。
- `sing install` 不下载或升级 `sing-box.exe`。

## `sc.exe` 错误
## `nssm.exe` 错误

Windows 服务注册和控制统一调用 `sc.exe`。调用时必须使用参数列表:
Windows 服务注册和控制统一调用 `nssm.exe`。调用时必须使用参数列表:

```python
subprocess.run(["sc.exe", "start", "sing-box"], ...)
subprocess.run(["nssm.exe", "start", "sing-box"], ...)
```

不要使用 shell 字符串拼接:

```python
subprocess.run(f"sc.exe start {service_name}", shell=True)
subprocess.run(f"nssm.exe start {service_name}", shell=True)
```

`sc.exe` 返回非零退出码时,CLI 命令失败,并把 stderr 或 stdout 中的系统错误摘要展示给用户。
`nssm.exe` 找不到或返回非零退出码时,CLI 命令失败,并把 stderr 或 stdout 中的系统错误摘要展示给用户。

## 服务状态错误

Expand All @@ -54,4 +54,4 @@ subprocess.run(f"sc.exe start {service_name}", shell=True)
- 捕获所有异常后继续执行,会让用户误以为服务已经安装或启动。
- 在服务已运行时让 `start` 自动重启,会隐藏用户选择的配置切换行为。
- 删除 active profile 会让 `state.json` 和 Windows 服务命令行分离。
- 把 `sc.exe` 命令拼成 shell 字符串会破坏带空格路径,并增加注入风险。
- 把 `nssm.exe` 命令拼成 shell 字符串会破坏带空格路径,并增加注入风险。
15 changes: 8 additions & 7 deletions .trellis/spec/backend/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
|------|------|------|
| [目录结构](./directory-structure.md) | 包布局、入口点、本地数据布局 | 已填写 |
| [数据库规范](./database-guidelines.md) | 无数据库约束、本地 JSON 状态边界 | 已填写 |
| [错误处理](./error-handling.md) | CLI 错误、`sc.exe` 错误、profile 状态错误 | 已填写 |
| [错误处理](./error-handling.md) | CLI 错误、NSSM 错误、profile 状态错误 | 已填写 |
| [质量规范](./quality-guidelines.md) | Python 版本、依赖、Ruff/ty/CI/Dependabot 契约、命令契约、测试要求 | 已填写 |
| [日志规范](./logging-guidelines.md) | CLI 输出边界、stdout/stderr 规则 | 已填写 |

Expand All @@ -27,7 +27,7 @@
- 可执行命令由 `pyproject.toml` 的 `[project.scripts]` 声明:`sing = "sing_cli.main:main"`。
- 当前源码位于 `src/sing_cli/`。
- CLI 框架使用 `typer`,HTTP 下载使用 `httpx`。
- Windows 服务管理使用系统内置 `sc.exe`,不使用 `pywin32`。
- Windows 服务管理使用 `nssm.exe`,不使用 `pywin32`。
- 固定 Windows 服务名为 `sing-box`。
- 本地应用数据目录使用 `typer.get_app_dir("sing-cli")`。

Expand All @@ -47,8 +47,8 @@ sing update <name>
sing list
```

- `sing install` 注册 `sing-box` Windows 服务并开启自启;默认使用 `PATH` 中的 `sing-box.exe`,`--bin` 可指定自定义路径。
- `sing install` 不下载、不升级 `sing-box.exe`,也不指定或写入业务 profile。
- `sing install` 通过 NSSM 注册 `sing-box` Windows 服务并开启自启;默认使用 `PATH` 中的 `sing-box.exe`,`--bin` 可指定自定义路径。
- `sing install` 不下载、不升级 `sing-box.exe` 或 `nssm.exe`,也不指定或写入业务 profile。
- `sing start <name>` 使用 `<name>` 对应的本地 profile 启动服务;服务已运行时失败并提示使用 `sing restart <name>`。
- `sing restart <name>` 停止当前服务后,用 `<name>` 对应 profile 重新启动。
- `sing stop` 停止服务,不需要 profile 名。
Expand All @@ -59,12 +59,13 @@ sing list

## 服务命令行

`sing start <name>` 和 `sing restart <name>` 启动前必须先更新服务命令行
`sing start <name>` 和 `sing restart <name>` 启动前必须先更新 NSSM 服务参数

```text
sc.exe config sing-box binPath= "<sing-box.exe> run -c <profile-path>"
nssm.exe set sing-box Application <sing-box.exe>
nssm.exe set sing-box AppParameters "run -c \"<profile-path>\""
```

随后再启动 `sing-box` 服务。Windows 自启时沿用最后一次 `sing start <name>` 或 `sing restart <name>` 写入的 profile。

调用 `sc.exe` 必须使用参数列表,不使用 shell 字符串拼接。
调用 `nssm.exe` 必须使用参数列表,不使用 shell 字符串拼接。
73 changes: 68 additions & 5 deletions .trellis/spec/backend/quality-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
- Ruff、ty 和 pytest 是当前已确认的代码质量工具。
- CLI 框架使用 `typer`。
- HTTP 客户端使用 `httpx`。
- Windows 服务管理使用 `sc.exe`,不使用 `pywin32`。
- Windows 服务管理使用 `nssm.exe`,不使用 `pywin32`。
- 文档和 Trellis SPEC 使用中文编写。

## 命令契约
Expand All @@ -31,12 +31,75 @@ sing update <name>
sing list
```

- `sing install` 只注册服务并开启自启;不处理业务配置。
- `sing start <name>` 负责把服务命令行更新到 `<name>` 对应配置,再启动服务。
- `sing restart <name>` 先停止服务,再更新服务命令行并启动服务
- `sing install` 通过 NSSM 注册服务并开启自启;不处理业务配置。
- `sing start <name>` 负责把 NSSM 服务参数更新到 `<name>` 对应配置,再启动服务。
- `sing restart <name>` 先停止服务,再更新 NSSM 服务参数并启动服务
- `sing stop` 不需要配置名。
- `sing list` 必须标出 active 配置。

## NSSM 服务管理契约

### 1. Scope / Trigger

- Trigger: 修改 Windows 服务安装、卸载、启动、停止、状态查询或 profile 到服务参数的映射。

### 2. Signatures

```text
nssm.exe install sing-box <sing-box.exe>
nssm.exe set sing-box Start SERVICE_AUTO_START
nssm.exe set sing-box Application <sing-box.exe>
nssm.exe set sing-box AppParameters "run -c \"<profile-path>\""
nssm.exe status sing-box
nssm.exe start sing-box
nssm.exe stop sing-box
nssm.exe remove sing-box confirm
```

### 3. Contracts

- `sing install [--bin <path>]` 解析 `sing-box.exe` 后调用 `nssm.exe install`,再设置 `Start` 为 `SERVICE_AUTO_START`。
- `sing start <name>` 启动前设置 `Application` 为已安装的 `sing-box.exe` 路径,设置 `AppParameters` 为 `run -c "<profile-path>"`。
- `sing restart <name>` 必须先停止服务,停止成功后再写入 NSSM 参数并启动服务。
- `nssm.exe` 必须从 `PATH` 解析;项目不下载、不内置 NSSM。
- 外部命令必须用参数列表调用,不通过 shell 字符串执行。

### 4. Validation & Error Matrix

| 条件 | 行为 |
|------|------|
| `nssm.exe` 不在 `PATH` | 命令失败并提示安装 NSSM 且让 `nssm.exe` 可从 `PATH` 访问 |
| `nssm.exe` 返回非零退出码 | 命令失败并暴露 stderr 或 stdout 摘要 |
| `nssm.exe status sing-box` 输出 `SERVICE_RUNNING` | `service_is_running()` 返回 `True` |
| `sing start <name>` 发现服务已运行 | 命令失败并提示使用 `sing restart <name>` |

### 5. Good/Base/Bad Cases

- Good: `subprocess.run([nssm_path, "set", "sing-box", "AppParameters", 'run -c "C:/profiles/home"'], ...)`
- Base: profile 路径包含空格时,`AppParameters` 仍作为单个参数传给 NSSM。
- Bad: `subprocess.run(f"nssm.exe set sing-box AppParameters run -c {profile_path}", shell=True)`

### 6. Tests Required

- 安装服务测试断言 `nssm.exe install sing-box <sing-box.exe>` 和 `nssm.exe set sing-box Start SERVICE_AUTO_START`。
- 配置服务测试断言 `Application` 与 `AppParameters` 分别写入。
- NSSM 失败测试断言非零退出码转换为 CLI 错误。
- NSSM 缺失测试断言错误信息说明 `nssm.exe` 不在 `PATH`。

### 7. Wrong vs Correct

#### Wrong

```python
subprocess.run(f"nssm.exe start {SERVICE_NAME}", shell=True)
```

#### Correct

```python
subprocess.run([nssm_path, "start", SERVICE_NAME], capture_output=True, check=False, text=True)
```

## Ruff 工具链契约

### 1. Scope / Trigger
Expand Down Expand Up @@ -430,7 +493,7 @@ updates:

- Typer 命令参数和错误提示。
- `sing install` 的 PATH 解析、`--bin` 覆盖、找不到二进制失败。
- `sc.exe` 参数列表构造,不通过 shell 字符串执行。
- `nssm.exe` 参数列表构造,不通过 shell 字符串执行。
- `sing start <name>` 服务已运行时失败。
- `sing restart <name>` 停止失败时不继续启动。
- `state.json` 读写、active 更新和删除 active 配置失败。
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Fix Windows Service Start Timeout

## Goal

Fix `sing start <name>` failing with Windows `StartService` error 1053 by using NSSM as the Windows service host while preserving the intended command model: `sing install` installs the Windows service and enables autostart, and `sing start <name>` starts `sing-box` with the selected configuration.

## What I Already Know

* User observed `sing start config.json` failing with `sc.exe failed: [SC] StartService failed 1053`.
* Current service registration and reconfiguration live in `src/sing_cli/service.py`.
* `install_service()` registers `sing-box` with `binPath= "<sing-box.exe> run"`.
* `configure_service()` rewrites the service to `binPath= "<sing-box.exe> run -c "<profile-path>""`.
* `sing-box run` is a normal foreground command and does not implement the Windows service control dispatcher expected by the Windows Service Control Manager.
* `sc.exe` operations are tested in `tests/test_service.py`.
* `sing install` is intended to install the Windows service and set it to autostart.
* `sing start <name>` is intended to use a saved configuration to start `sing-box`.
* NSSM supports command-line service installation with `nssm install <servicename> <program> [<arguments>]`.
* NSSM service parameters can be updated with `nssm set <servicename> <parameter> <value>`, including `Application`, `AppDirectory`, `AppParameters`, and `Start`.
* NSSM supports `nssm start <servicename>`, `nssm stop <servicename>`, and `nssm remove <servicename> confirm`.

## Assumptions

* The CLI should keep the existing command surface: `sing install`, `sing start <name>`, `sing stop`, and `sing restart <name>`.
* The fix should preserve the fixed Windows service name `sing-box`.
* The service should run the installed `sing-box.exe` with the selected profile path.
* Autostart should use the last configuration applied by `sing start <name>` or `sing restart <name>`.
* No backward compatibility with the old direct `sc.exe binPath` format is required.
* `nssm.exe` is available on `PATH` when Windows service commands are used.

## Requirements

* Register the `sing-box` Windows service through NSSM.
* Keep `sing install` focused on service installation and autostart setup.
* Make `sing start <name>` configure the selected profile and start the Windows service without triggering SCM error 1053.
* Make `sing restart <name>` stop, reconfigure, and start through the same service host.
* Ensure NSSM starts the configured `sing-box.exe run -c <profile>` command.
* Keep external command failures visible as explicit CLI errors.
* Add or update tests for the service command construction and configuration behavior.

## Acceptance Criteria

* [ ] `sing install --bin <path>` calls NSSM to install the `sing-box` service.
* [ ] `sing install --bin <path>` sets NSSM `Start` to `SERVICE_AUTO_START`.
* [ ] `sing start <name>` sets NSSM `Application` and `AppParameters` so NSSM runs `<bin> run -c <profile>`.
* [ ] `sing start <name>` starts the service through NSSM.
* [ ] Unit tests cover the updated install/configure/start command behavior.
* [ ] Ruff, ty, and pytest pass.

## Definition of Done

* Tests added or updated where behavior changes.
* Lint, type check, and tests pass.
* Documentation is updated if the public command behavior changes.
* Windows-specific behavior remains testable on non-Windows through injected command runners.

## Out of Scope

* Downloading or upgrading `sing-box.exe`.
* Supporting multiple Windows service instances.
* Silent fallback to foreground process execution when service startup fails.
* Adding compatibility for the old `binPath= "<sing-box.exe> run"` service format.
* Downloading or bundling `nssm.exe`.

## Technical Notes

* Relevant files: `src/sing_cli/service.py`, `src/sing_cli/cli.py`, `tests/test_service.py`, `README.md`.
* Project spec index: `.trellis/spec/backend/index.md`.
* The current implementation uses `sc.exe` with argument lists; NSSM should also be called with argument lists and no shell string.
* NSSM command reference: `https://www.nssm.cc/commands`.
* NSSM usage reference: `https://nssm.cc/usage`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"id": "fix-windows-service-start-timeout",
"name": "fix-windows-service-start-timeout",
"title": "Fix Windows service start timeout",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "pixelcola",
"assignee": "pixelcola",
"createdAt": "2026-05-12",
"completedAt": "2026-05-12",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
5 changes: 3 additions & 2 deletions .trellis/workspace/pixelcola/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<!-- @@@auto:current-status -->
- **Active File**: `journal-1.md`
- **Total Sessions**: 2
- **Total Sessions**: 3
- **Last Active**: 2026-05-12
<!-- @@@/auto:current-status -->

Expand All @@ -19,7 +19,7 @@
<!-- @@@auto:active-documents -->
| File | Lines | Status |
|------|-------|--------|
| `journal-1.md` | ~73 | Active |
| `journal-1.md` | ~106 | Active |
<!-- @@@/auto:active-documents -->

---
Expand All @@ -29,6 +29,7 @@
<!-- @@@auto:session-history -->
| # | Date | Title | Commits | Branch |
|---|------|-------|---------|--------|
| 3 | 2026-05-12 | Use NSSM for Windows service | `72c1b0d` | `fix/use-nssm-service` |
| 2 | 2026-05-12 | Windows sing-box CLI | `2f18462` | `feature/windows-sing-box-cli` |
| 1 | 2026-05-12 | Bootstrap Guidelines | `ced349d` | `main` |
<!-- @@@/auto:session-history -->
Expand Down
33 changes: 33 additions & 0 deletions .trellis/workspace/pixelcola/journal-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,36 @@ Implemented the Windows sing-box CLI with Typer commands, local profile state, s
### Next Steps

- None - task complete


## Session 3: Use NSSM for Windows service

**Date**: 2026-05-12
**Task**: Use NSSM for Windows service
**Branch**: `fix/use-nssm-service`

### Summary

Replaced direct sc.exe service management with NSSM commands, updated service tests and documented the NSSM service contract.

### Main Changes

(Add details)

### Git Commits

| Hash | Message |
|------|---------|
| `72c1b0d` | (see git log) |

### Testing

- [OK] (Add test results)

### Status

[OK] **Completed**

### Next Steps

- None - task complete
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

Windows CLI for installing and controlling the `sing-box` Windows service.

## Requirements

`sing-box.exe` and `nssm.exe` must be available in `PATH`. `sing install --bin <path>` can use a custom `sing-box.exe` path.

## Commands

| Command | Description |
|---|---|
| `sing install [--bin <path>]` | Register the `sing-box` Windows service and enable autostart. |
| `sing install [--bin <path>]` | Register the `sing-box` Windows service through NSSM and enable autostart. |
| `sing uninstall` | Delete the `sing-box` Windows service. |
| `sing start <name>` | Start the service with a saved profile. |
| `sing stop` | Stop the service. |
Expand All @@ -16,4 +20,4 @@ Windows CLI for installing and controlling the `sing-box` Windows service.
| `sing update <name>` | Redownload a saved profile from its URL. |
| `sing list` | List saved profiles and mark the active profile. |

`sing install` uses `sing-box.exe` from `PATH` unless `--bin` is provided. The CLI does not download or upgrade `sing-box.exe`.
`sing install` uses `sing-box.exe` from `PATH` unless `--bin` is provided. The CLI does not download or upgrade `sing-box.exe` or `nssm.exe`.
Loading