From 9169ba3c7b793bf5d48123a99d01a10b17456a0e Mon Sep 17 00:00:00 2001 From: Trae User Date: Sat, 19 Sep 2026 12:45:34 +0800 Subject: [PATCH 1/2] fix: centralize TUI runtime logs --- CONTEXT.md | 13 +++ docs/adr/0002-feedback-origin-log-boundary.md | 3 + docs/daemon-api.md | 13 +-- docs/user-manual.md | 13 ++- external/ftxui | 2 +- .../centralize-tui-logs/.openspec.yaml | 2 + .../changes/centralize-tui-logs/design.md | 91 +++++++++++++++++ .../changes/centralize-tui-logs/proposal.md | 34 +++++++ .../specs/tui-runtime-logging/spec.md | 97 +++++++++++++++++++ openspec/changes/centralize-tui-logs/tasks.md | 32 ++++++ ports/ftxui/portfile.cmake | 1 + ports/ftxui/vcpkg.json | 2 +- src/commands/builtin_commands.cpp | 14 +-- src/feedback/feedback_upload.cpp | 36 +++++-- src/feedback/feedback_upload.hpp | 9 +- src/main.cpp | 8 +- src/tui/tui_init.cpp | 8 +- tests/feedback/feedback_upload_test.cpp | 50 ++++++++++ tests/utils/logger_test.cpp | 26 +++++ 19 files changed, 420 insertions(+), 34 deletions(-) create mode 100644 CONTEXT.md create mode 100644 docs/adr/0002-feedback-origin-log-boundary.md create mode 100644 openspec/changes/centralize-tui-logs/.openspec.yaml create mode 100644 openspec/changes/centralize-tui-logs/design.md create mode 100644 openspec/changes/centralize-tui-logs/proposal.md create mode 100644 openspec/changes/centralize-tui-logs/specs/tui-runtime-logging/spec.md create mode 100644 openspec/changes/centralize-tui-logs/tasks.md diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..1233a7b6 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,13 @@ +# ACECode + +ACECode is a local AI coding agent with terminal, desktop, daemon, and headless runtime surfaces. This context records product terms that distinguish those surfaces and their diagnostic artifacts. + +## Language + +**Feedback origin**: +The runtime surface that initiates a diagnostic feedback package: `tui` for the terminal command and `desktop` for the GUI/Desktop endpoint. +_Avoid_: Feedback client, UI type + +**Surface log**: +The dated runtime log produced by one ACECode surface, named with that surface's prefix in the effective data directory's `logs` subdirectory. +_Avoid_: Shared UI log, workspace log diff --git a/docs/adr/0002-feedback-origin-log-boundary.md b/docs/adr/0002-feedback-origin-log-boundary.md new file mode 100644 index 00000000..264c72e4 --- /dev/null +++ b/docs/adr/0002-feedback-origin-log-boundary.md @@ -0,0 +1,3 @@ +# Feedback packages include only their originating surface log + +TUI feedback includes the latest TUI surface log, while GUI/Desktop feedback includes the latest Desktop surface log; neither includes the other surface's log. This keeps diagnostic attachments relevant to the reporting surface and avoids unnecessarily sending diagnostics from an unrelated interface. diff --git a/docs/daemon-api.md b/docs/daemon-api.md index 1bdf7aff..c37c21d8 100644 --- a/docs/daemon-api.md +++ b/docs/daemon-api.md @@ -3796,12 +3796,13 @@ Body fields are optional strings: } ``` -The package always carries the newest rotated log of every runtime that writes -into the logs directory: the desktop shell (`desktop-.log`) and the daemon -that serves the request (`daemon-.log`). Each is truncated to its last -512 KiB and stored as `logs/desktop.log.tail.txt` / `logs/daemon.log.tail.txt`. -A runtime with no log file present is skipped silently, so a browser-only -deployment uploads the daemon log alone. Upgrade diagnostics +The package carries the newest rotated Desktop shell log (`desktop-.log`) +and the daemon log (`daemon-.log`) that serves the request. Each is +truncated to its last 512 KiB and stored as `logs/desktop.log.tail.txt` / +`logs/daemon.log.tail.txt`. A runtime with no log file present is skipped +silently, so a browser-only deployment uploads the daemon log alone. Desktop +feedback does not attach `tui-.log`; terminal `/feedback` instead carries +`tui-.log` plus the daemon log and does not attach the Desktop log. Upgrade diagnostics (`upgrade--.log`, one file per process) are handled as a window rather than a single newest file: every upgrade log modified within the last three days (72 hours) is merged oldest-first into `logs/upgrade.log.tail.txt`, diff --git a/docs/user-manual.md b/docs/user-manual.md index efbb01e2..e26af63a 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -720,7 +720,7 @@ acecode 启动时按 transport 连接每个配置的 MCP 服务器: [MCP] Connected 2 server(s), registered 8 external tool(s). ``` -若某个服务器连接失败,可在当前工作目录的 `acecode.log` 中查看详细错误信息。 +若某个服务器连接失败,可在 `<数据目录>/logs/tui-YYYY-MM-DD.log` 中查看详细错误信息(默认数据目录为 `~/.acecode`)。 ### 管理 MCP 连接 @@ -856,9 +856,14 @@ OpenAI 兼容网关或 Anthropic 模型如果推理时间很长、连接容易 ### Q: 日志文件在哪里? -acecode 运行时会在当前工作目录生成 `acecode.log` 文件,记录详细的调试信息。 -Daemon 模式下不再写 `acecode.log`,而是按日期滚动写到 -`<数据目录>/logs/daemon-{YYYY-MM-DD}.log`(详见第 16 章)。 +普通 TUI 运行时会按本地日期滚动写入 +`<数据目录>/logs/tui-YYYY-MM-DD.log`,记录详细的调试信息。默认数据目录为 +`~/.acecode`;若已配置数据目录重定向,则使用重定向后的目录。已有工作区中的 +`acecode.log` 会被保留,但新的 TUI 不再创建或追加该文件。启用 TUI 输入追踪的 +调试构建会将高频鼠标和选区记录另写到 +`<数据目录>/logs/tui-input-trace-YYYY-MM-DD.log`,同样不会写入工作区。 + +Daemon 模式按日期滚动写到 `<数据目录>/logs/daemon-YYYY-MM-DD.log`(详见第 16 章)。 --- diff --git a/external/ftxui b/external/ftxui index f98588b4..ca7fbc06 160000 --- a/external/ftxui +++ b/external/ftxui @@ -1 +1 @@ -Subproject commit f98588b47b097674724100c7a35a3c9b8ec055e3 +Subproject commit ca7fbc06e82aee78c3f96c1a169366aba972c379 diff --git a/openspec/changes/centralize-tui-logs/.openspec.yaml b/openspec/changes/centralize-tui-logs/.openspec.yaml new file mode 100644 index 00000000..f2cbbe6a --- /dev/null +++ b/openspec/changes/centralize-tui-logs/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-18 diff --git a/openspec/changes/centralize-tui-logs/design.md b/openspec/changes/centralize-tui-logs/design.md new file mode 100644 index 00000000..24e2e568 --- /dev/null +++ b/openspec/changes/centralize-tui-logs/design.md @@ -0,0 +1,91 @@ +# Design: centralize-tui-logs + +## Context + +The normal interactive startup path calls a file-static `initialize_logger_for_working_dir()` in `src/main.cpp`. It selects `Logger::init(working_dir + "/acecode.log")`, the legacy single-file mode. That explicit choice, rather than a logger fallback, causes project-local logs. `src/tui/tui_init.cpp` contains a duplicate helper with the same legacy behavior, although the current main path uses the file-static copy. + +`Logger::init_with_rotation(get_logs_dir(), base_name, mirror_stderr)` is already used by desktop (`desktop`), daemon (`daemon`), and headless (`headless`). It creates the directory, names files `-YYYY-MM-DD.log`, and reopens a new file when the local date changes. `get_logs_dir()` resolves from `get_acecode_dir()`, preserving the application-wide user/service data-root behavior. + +TUI `/feedback` separately adds `ctx.cwd/acecode.log`, while the shared runtime-log collector finds desktop and daemon logs for GUI/Desktop feedback. The selected feedback boundary is origin-aware: TUI feedback must attach the centralized TUI log and must not attach Desktop logs; GUI/Desktop feedback must retain Desktop and daemon logs but must not attach TUI logs. This must move with the primary logger so TUI diagnostics remain attached after centralization. + +## Goals / Non-Goals + +**Goals:** + +- Make normal interactive TUI runtime logs follow the same centralized data-directory convention as other runtime surfaces. +- Keep TUI records separate from desktop, daemon, and headless records via a `tui` filename prefix. +- Preserve legacy workspace files without filesystem migration or deletion. +- Ensure each feedback package contains its own surface log but not an unrelated interface's surface log. +- Keep the change localized, testable, and independent of frontend surfaces. + +**Non-Goals:** + +- Do not change the logger's legacy `init(file)` API; tests or specialized callers may still use it. +- Do not change which input events the opt-in `ACECODE_TUI_INPUT_TRACE` instrumentation records or its trace message format. +- Do not add retention, compression, or a user-configurable logging directory. +- Do not merge TUI logs with desktop or daemon logs. + +## Decisions + +### D1. Use the existing rotated logger with the `tui` base name + +Both TUI logger initialization helpers will use: + +```cpp +Logger::instance().init_with_rotation(get_logs_dir(), "tui", false); +``` + +This gives TUI logs the path `/logs/tui-YYYY-MM-DD.log`, automatically creates the directory, rotates at local midnight, and retains the current behavior of not writing log records to stderr. `working_dir` remains useful for the startup log message and later workspace setup, but no longer determines the log file path. + +**Alternative considered:** keep a fixed `/logs/acecode.log`. Rejected because the existing runtime convention is per-surface dated files, and a TUI-specific prefix keeps simultaneous surfaces diagnosable. + +### D2. Reconcile the duplicated TUI initialization helpers + +The implementation must prevent `src/main.cpp` and `src/tui/tui_init.cpp` from continuing to encode divergent logger destinations. Prefer one shared TUI initialization helper when its existing dependency boundaries permit it; otherwise update both helpers to the same centralized rotated initialization and retain a focused regression test for the active startup seam. + +The work must not leave a future path capable of silently restoring workspace-local primary logs. + +### D3. Make runtime-log discovery feedback-origin-aware + +Keep the existing GUI/Desktop runtime-log collector limited to `desktop` and `daemon`, preserving its documented REST contract. Add a TUI-specific collector (or an equivalent explicit source selector) that returns the latest `tui-YYYY-MM-DD.log` as `logs/tui.log.tail.txt` together with the daemon log, but excludes the Desktop log. Remove the TUI command's bespoke `ctx.cwd/acecode.log` source. + +Both selectors use `latest_rotated_log_path` so matching and newest-file rules remain centralized. Missing logs remain non-fatal. This yields two explicit source sets: + +- TUI `/feedback`: TUI + daemon + recent upgrade diagnostics. +- GUI/Desktop feedback: Desktop + daemon + recent upgrade diagnostics. + +**Alternative considered:** add `tui` to `collect_runtime_log_sources(logs_dir)`. Rejected because that shared function backs the GUI/Desktop REST endpoint; it would silently broaden GUI diagnostic collection with unrelated terminal logs. Another alternative was a bespoke TUI path lookup in the command; rejected because it duplicates date-file matching and latest-file selection. + +### D4. Configure FTXUI tracing through the process environment + +FTXUI is built as a vcpkg dependency and cannot depend directly on ACECode's C++ configuration helpers. Add a dedicated environment variable, set by interactive TUI startup before FTXUI processes input, whose value is the UTF-8 absolute logs directory. FTXUI's trace writer reads that directory and creates/appends `tui-input-trace-YYYY-MM-DD.log` for the current local date on each trace write. If the variable is absent or the target file cannot be opened, tracing remains best-effort and creates no relative fallback file. + +This preserves a clear dependency boundary: ACECode owns effective data-directory resolution, while FTXUI owns only local-date filename generation and appending trace records below the explicitly supplied directory. The trace writer should create the target directory before opening its file. An explicit absolute directory prevents worktree cwd changes from affecting the destination. + +**Alternative considered:** link FTXUI against ACECode's logger/configuration utilities. Rejected because the dependency direction reverses the build boundary and would make the reusable FTXUI target depend on the application. Another alternative was a compile-time path definition; rejected because the effective data directory may be redirected at runtime and the date changes while a TUI process remains open. + +### D5. Bump the FTXUI overlay port version + +The overlay port builds `external/ftxui` from `SOURCE_PATH`, but vcpkg's ABI does not hash that source tree. Increment `ports/ftxui/vcpkg.json` `port-version` whenever the FTXUI source changes, ensuring existing local builds and CI rebuild the patched static library rather than reusing one that still writes `acecode.log` in the cwd. + +### D6. Document the effective data-directory placeholder + +Update the user manual's FAQ and MCP troubleshooting text to use `<数据目录>/logs/tui-YYYY-MM-DD.log`. Document the input-trace file separately as an opt-in debug artifact. Both descriptions use the effective data directory, which is accurate across supported platforms and data-root redirection, unlike hardcoding a Windows-only home path. + +## Risks / Trade-offs + +- **Feedback archive name change:** consumers that recognize `logs/acecode.log.tail.txt` will no longer see a TUI legacy entry. The old file was workspace-specific and is no longer a valid runtime source; a distinct `logs/tui.log.tail.txt` accurately identifies the new source. The high-volume FTXUI input trace remains a local diagnostic artifact and is not attached by either feedback origin. +- **Startup ordering:** TUI logger initialization remains before normal configuration loading, as it is today. `get_logs_dir()` only uses existing data-directory resolution and does not require loaded application configuration. +- **Old docs and external scripts:** workspace-local `acecode.log` may be referenced externally. The selected migration policy intentionally preserves old files but does not maintain an active compatibility copy, preventing further workspace pollution. + +## Migration Plan + +On the first upgraded interactive TUI launch, ACECode writes to the centralized dated TUI file. Existing workspace `acecode.log` files are untouched. `/feedback` begins attaching the latest centralized TUI log when available. No configuration or session migration is required. + +## Validation + +- Unit-test logger rotation behavior using the established `Logger` test fixture and add a targeted assertion for `tui` file naming. +- Unit-test feedback runtime source discovery with TUI, desktop, and daemon files, including latest-file selection and missing-TUI behavior. +- Add or adjust a focused TUI startup helper test so the production initialization chooses `get_logs_dir()` and the `tui` base name without needing an interactive terminal. +- Run the focused native tests and the repository's relevant build target. +- Verify documentation contains no remaining normal-TUI instructions to inspect workspace-local `acecode.log`. diff --git a/openspec/changes/centralize-tui-logs/proposal.md b/openspec/changes/centralize-tui-logs/proposal.md new file mode 100644 index 00000000..d0b14a46 --- /dev/null +++ b/openspec/changes/centralize-tui-logs/proposal.md @@ -0,0 +1,34 @@ +# Proposal: centralize-tui-logs + +## Why + +Interactive TUI startup currently writes a non-rotating `acecode.log` into the active workspace. This pollutes project directories, including startup worktrees, and differs from Desktop, daemon, and headless modes, which write dated logs in the ACECode data directory. The TUI feedback command also packages the workspace-local file, so it would omit the relevant diagnostics after the logging destination changes unless its source is updated together. + +## What Changes + +- Move the interactive TUI primary logger from `/acecode.log` to `/logs/tui-YYYY-MM-DD.log`. +- Reuse the existing rotating logger behavior: create the shared logs directory as needed, rotate at local midnight, and do not mirror TUI records to stderr. +- Preserve existing workspace `acecode.log` files without migrating or deleting them; future normal TUI runs no longer create or append to them. +- Change TUI `/feedback` to attach the latest rotated TUI log as `logs/tui.log.tail.txt` instead of attaching `/acecode.log`; it continues to attach the daemon and recent upgrade diagnostics but no longer attaches Desktop logs. +- Keep GUI/Desktop feedback limited to Desktop, daemon, and recent upgrade diagnostics; it must not attach TUI logs. +- Move opt-in FTXUI input tracing from a relative `acecode.log` in the active workspace to an independent centralized daily file, `/logs/tui-input-trace-YYYY-MM-DD.log`. +- Update user documentation and test coverage for the centralized TUI location and feedback source. + +## Capabilities + +### New Capabilities + +- `tui-runtime-logging`: Centralized, dated TUI runtime logs and their feedback-package inclusion. + +### Modified Capabilities + +(None.) + +## Impact + +- **TUI startup:** `src/main.cpp` and the duplicate TUI logger initialization helper in `src/tui/tui_init.cpp`. +- **FTXUI input tracing:** `external/ftxui/src/ftxui/component/app.cpp`, its CMake definitions, and `ports/ftxui/` overlay-version metadata. +- **Feedback:** `src/commands/builtin_commands.cpp` and the rotated-log discovery helpers in `src/feedback/feedback_upload.*`. +- **Tests:** logger, feedback-package, and a focused TUI initialization seam if needed for testability. +- **Documentation:** `docs/user-manual.md`; update `docs/daemon-api.md` if its feedback attachment contract is expanded to name the TUI runtime log. +- **Compatibility:** existing FTXUI trace message format remains unchanged; only its output destination and daily rotation behavior change. diff --git a/openspec/changes/centralize-tui-logs/specs/tui-runtime-logging/spec.md b/openspec/changes/centralize-tui-logs/specs/tui-runtime-logging/spec.md new file mode 100644 index 00000000..fb058742 --- /dev/null +++ b/openspec/changes/centralize-tui-logs/specs/tui-runtime-logging/spec.md @@ -0,0 +1,97 @@ +# tui-runtime-logging Spec + +## ADDED Requirements + +### Requirement: Interactive TUI logs use the ACECode data directory + +The interactive TUI SHALL initialize its primary logger with the effective ACECode data directory's `logs` subdirectory and the `tui` base name. It MUST create or append to a local-date file named `tui-YYYY-MM-DD.log`, rotate to a new file after local midnight, and MUST NOT mirror normal TUI log records to stderr. + +The effective data directory MUST follow the existing `get_acecode_dir()` run-mode resolution, so normal user launches use the user ACECode data directory rather than the active workspace or startup worktree. + +#### Scenario: Normal TUI startup does not pollute the workspace + +- **WHEN** a user starts the interactive TUI in a workspace that has no `acecode.log` +- **THEN** the TUI creates or appends `/logs/tui-YYYY-MM-DD.log` +- **AND THEN** it does not create `/acecode.log` + +#### Scenario: Startup worktree does not become the log destination + +- **WHEN** a user starts the interactive TUI with `--worktree ` +- **THEN** the TUI log is written below the effective ACECode data directory's `logs` directory +- **AND THEN** no primary TUI log is created inside the newly selected worktree + +#### Scenario: Existing workspace log is preserved + +- **WHEN** `/acecode.log` already exists before interactive TUI startup +- **THEN** ACECode does not move, delete, or append to that file as part of logger initialization + +### Requirement: Opt-in FTXUI input tracing uses a separate centralized log + +When `ACECODE_TUI_INPUT_TRACE` is enabled, FTXUI SHALL write its input-event trace to `/logs/tui-input-trace-YYYY-MM-DD.log`. It MUST create the logs directory when needed and MUST rotate to the local-date file after local midnight. It MUST NOT create or append a relative `acecode.log` in the active workspace or startup worktree. + +The input-trace file SHALL remain distinct from `tui-YYYY-MM-DD.log`, so high-frequency mouse and selection diagnostics do not obscure normal TUI runtime logs. It SHALL NOT be attached to TUI or GUI/Desktop feedback packages. Existing trace record text and its `DBG [ftxui-app]` prefix SHALL remain unchanged. + +#### Scenario: Input tracing does not write inside a normal workspace + +- **WHEN** TUI input tracing is enabled and a user starts the TUI in a workspace +- **THEN** trace records are written to `/logs/tui-input-trace-YYYY-MM-DD.log` +- **AND THEN** no trace-created `/acecode.log` exists + +#### Scenario: Input tracing does not write inside a startup worktree + +- **WHEN** TUI input tracing is enabled and a user starts the TUI with `--worktree ` +- **THEN** trace records are written to the effective ACECode data directory's `logs` subdirectory +- **AND THEN** no trace-created `acecode.log` exists within the selected worktree + +#### Scenario: Input tracing remains separate from primary TUI logs + +- **WHEN** TUI input tracing and normal TUI logging both emit records +- **THEN** FTXUI trace records appear only in `tui-input-trace-YYYY-MM-DD.log` +- **AND THEN** primary runtime records continue to appear in `tui-YYYY-MM-DD.log` + +### Requirement: TUI feedback includes the centralized TUI log + +The TUI `/feedback` workflow SHALL find the most recently modified rotated TUI log in the effective ACECode data directory's `logs` subdirectory and include it in the feedback package as `logs/tui.log.tail.txt` when available. It MUST NOT add `/acecode.log` as a normal TUI feedback source. + +Failure to find a rotated TUI log MUST remain non-fatal and MUST NOT prevent collection of daemon or upgrade logs. TUI feedback MUST NOT attach a Desktop surface log, and GUI/Desktop feedback MUST NOT attach a TUI surface log. + +#### Scenario: Feedback package includes the latest TUI log + +- **WHEN** the logs directory contains multiple `tui-YYYY-MM-DD.log` files +- **AND WHEN** a user runs `/feedback` +- **THEN** the package includes the most recently modified matching file under `logs/tui.log.tail.txt` + +#### Scenario: Legacy workspace log is not attached + +- **WHEN** the workspace contains an old `acecode.log` +- **AND WHEN** a rotated TUI log is available in the data directory +- **THEN** the feedback package attaches the rotated TUI log +- **AND THEN** it does not attach the workspace-local legacy log + +#### Scenario: TUI feedback excludes Desktop logs + +- **WHEN** a TUI user runs `/feedback` +- **AND WHEN** a `desktop-YYYY-MM-DD.log` file is available +- **THEN** the package does not attach `logs/desktop.log.tail.txt` + +#### Scenario: Desktop feedback excludes TUI logs + +- **WHEN** a GUI/Desktop user submits feedback +- **AND WHEN** a `tui-YYYY-MM-DD.log` file is available +- **THEN** the package does not attach `logs/tui.log.tail.txt` + +#### Scenario: Missing TUI log does not block feedback + +- **WHEN** no `tui-YYYY-MM-DD.log` exists in the logs directory +- **THEN** feedback packaging continues without `logs/tui.log.tail.txt` +- **AND THEN** other available daemon or upgrade log sources continue to be collected + +### Requirement: User documentation describes the unified runtime log location + +User-facing documentation SHALL describe the interactive TUI log as `/logs/tui-YYYY-MM-DD.log` and direct TUI troubleshooting, including MCP connection troubleshooting, to the centralized runtime logs directory. It MUST NOT describe normal TUI runtime logging as writing `/acecode.log`. + +#### Scenario: Log-location documentation is consistent + +- **WHEN** a user reads the logging FAQ or MCP troubleshooting instructions +- **THEN** both identify the centralized TUI dated log location +- **AND THEN** neither instructs the user to inspect a workspace-local `acecode.log` for normal TUI runtime logs diff --git a/openspec/changes/centralize-tui-logs/tasks.md b/openspec/changes/centralize-tui-logs/tasks.md new file mode 100644 index 00000000..56298b6e --- /dev/null +++ b/openspec/changes/centralize-tui-logs/tasks.md @@ -0,0 +1,32 @@ +# Tasks: centralize-tui-logs + +## 1. TUI logger initialization + +- [x] 1.1 Replace the interactive TUI startup use of workspace-local `Logger::init(.../acecode.log)` with `Logger::init_with_rotation(get_logs_dir(), "tui", false)` while preserving debug log level and startup diagnostics. +- [x] 1.2 Reconcile the duplicate initialization helper in `src/tui/tui_init.*` so it cannot retain or reintroduce workspace-local primary logging. +- [x] 1.3 Add focused native coverage proving the TUI logger uses the shared logs directory, `tui-YYYY-MM-DD.log` naming, no stderr mirror, and does not select the workspace as the primary log destination. + +## 2. FTXUI input-trace destination + +- [x] 2.1 Add a TUI-startup environment bridge that supplies FTXUI the effective ACECode logs directory before input processing starts. +- [x] 2.2 Change the opt-in FTXUI trace writer to create and append `tui-input-trace-YYYY-MM-DD.log` below that directory, with local-date rollover and no relative-path fallback. +- [x] 2.3 Bump the FTXUI overlay port version so vcpkg rebuilds the source-backed dependency. +- [ ] 2.4 Add focused coverage for trace-path selection and local-date filename generation without requiring an interactive terminal; verify an input-trace-enabled build does not create workspace/worktree `acecode.log`. + +## 3. Feedback runtime-log collection + +- [x] 3.1 Preserve the GUI/Desktop runtime-log selector as Desktop + daemon only; add a TUI feedback selector that returns latest TUI + daemon logs and excludes Desktop logs. +- [x] 3.2 Remove the TUI `/feedback` command's explicit `/acecode.log` source and use the TUI centralized selector. +- [x] 3.3 Add feedback-package tests covering latest TUI log selection, TUI exclusion of Desktop logs, GUI/Desktop exclusion of TUI logs, missing TUI logs, and exclusion of a workspace-local legacy `acecode.log`. + +## 4. Documentation + +- [x] 4.1 Update the user-manual logging FAQ to describe `<数据目录>/logs/tui-YYYY-MM-DD.log`, the opt-in `tui-input-trace-YYYY-MM-DD.log`, and the preservation of legacy workspace files. +- [x] 4.2 Update MCP failure troubleshooting text to direct users to the centralized TUI dated log. +- [x] 4.3 Update daemon API feedback-package documentation if its runtime-log attachment listing needs to name `logs/tui.log.tail.txt`. + +## 5. Verification + +- [ ] 5.1 Run the focused logger, FTXUI trace-path, and feedback native tests. +- [ ] 5.2 Configure and build an input-trace-enabled TUI executable with the bumped FTXUI port, then verify normal-workspace and `--worktree` runs leave no trace-created `acecode.log`. +- [x] 5.3 Review the documentation diff and verify normal-TUI and FTXUI input-trace documentation no longer directs users to workspace-local `acecode.log`. diff --git a/ports/ftxui/portfile.cmake b/ports/ftxui/portfile.cmake index 8d2fd0ce..6c55b365 100644 --- a/ports/ftxui/portfile.cmake +++ b/ports/ftxui/portfile.cmake @@ -7,6 +7,7 @@ # 因此:**每次 external/ftxui 的 gitlink 变更,必须同步 bump vcpkg.json 的 # port-version**,否则改动不会生效。当前对应子模块 commit: # f98588b4 (v7.0.3-22,含 synchronized output + opt-in mouse hover motion) +# —— 输入追踪日志路径由 ACECODE_FTXUI_INPUT_TRACE_DIR 运行时指定。 # —— hover-motion 补丁(link-hover-tooltip 依赖无按键 Mouse::Moved 上报, # DEC mode 1003),提供 ftxui::App::EnableMouseHoverMotion()。 vcpkg_check_linkage(ONLY_STATIC_LIBRARY) diff --git a/ports/ftxui/vcpkg.json b/ports/ftxui/vcpkg.json index 347bf6e9..e35e81fe 100644 --- a/ports/ftxui/vcpkg.json +++ b/ports/ftxui/vcpkg.json @@ -1,7 +1,7 @@ { "name": "ftxui", "version-semver": "7.0.3", - "port-version": 5, + "port-version": 7, "description": "C++ Functional Terminal User Interface (shaohaozhi286 fork; adds ACECode TUI selection, input trace, Kitty keyboard lifecycle, and synchronized output (DEC mode 2026) patches — see external/ftxui/ACECODE_PATCHES.md)", "homepage": "https://github.com/shaohaozhi286/FTXUI", "license": "MIT", diff --git a/src/commands/builtin_commands.cpp b/src/commands/builtin_commands.cpp index 1fc3bec5..9494fc1a 100644 --- a/src/commands/builtin_commands.cpp +++ b/src/commands/builtin_commands.cpp @@ -501,18 +501,10 @@ static void cmd_feedback(CommandContext& ctx, const std::string& raw_args) { package_req.session_id = session_id; package_req.session_jsonl_path = session_jsonl; package_req.acecode_version = ACECODE_VERSION; - // TUI 自己的日志(cwd/acecode.log)+ 同机 daemon / desktop 的滚动日志: - // TUI 会话也可能被 daemon 侧的组件影响,缺失的来源会被静默跳过。 - { - acecode::feedback::FeedbackLogSource tui_log; - tui_log.path = path_from_utf8(ctx.cwd) / "acecode.log"; - tui_log.entry_name = "logs/acecode.log.tail.txt"; - package_req.logs.push_back(std::move(tui_log)); - } + // TUI feedback carries its own surface log plus the shared daemon log, but not + // an unrelated Desktop surface log. Missing sources are skipped. const fs::path logs_dir = path_from_utf8(get_logs_dir()); - for (auto& source : acecode::feedback::collect_runtime_log_sources(logs_dir)) { - package_req.logs.push_back(std::move(source)); - } + package_req.logs = acecode::feedback::collect_tui_runtime_log_sources(logs_dir); // 最近三天的升级记录合并成一个条目:「更新之后就不对了」这类反馈要看的就是它。 if (auto upgrade_logs = acecode::feedback::collect_recent_upgrade_log_bundle(logs_dir)) { package_req.log_bundles.push_back(std::move(*upgrade_logs)); diff --git a/src/feedback/feedback_upload.cpp b/src/feedback/feedback_upload.cpp index 15529b3e..69f15086 100644 --- a/src/feedback/feedback_upload.cpp +++ b/src/feedback/feedback_upload.cpp @@ -285,14 +285,15 @@ std::optional latest_desktop_log_path(const fs::path& logs_dir) { return latest_rotated_log_path(logs_dir, "desktop"); } -std::vector collect_runtime_log_sources(const fs::path& logs_dir) { - // entry 名固定,不随日志文件的日期后缀变化 —— 服务端按名取用。 - const std::pair wanted[] = { - {"desktop", "logs/desktop.log.tail.txt"}, - {"daemon", "logs/daemon.log.tail.txt"}, - }; +namespace { + +std::vector collect_surface_runtime_log_sources( + const fs::path& logs_dir, + const std::pair* wanted, + std::size_t wanted_count) { std::vector sources; - for (const auto& [base, entry_name] : wanted) { + for (std::size_t i = 0; i < wanted_count; ++i) { + const auto& [base, entry_name] = wanted[i]; if (auto path = latest_rotated_log_path(logs_dir, base)) { FeedbackLogSource source; source.path = *path; @@ -303,6 +304,27 @@ std::vector collect_runtime_log_sources(const fs::path& logs_ return sources; } +} // namespace + +std::vector collect_runtime_log_sources(const fs::path& logs_dir) { + // entry 名固定,不随日志文件的日期后缀变化 —— 服务端按名取用。 + const std::pair wanted[] = { + {"desktop", "logs/desktop.log.tail.txt"}, + {"daemon", "logs/daemon.log.tail.txt"}, + }; + return collect_surface_runtime_log_sources( + logs_dir, wanted, sizeof(wanted) / sizeof(wanted[0])); +} + +std::vector collect_tui_runtime_log_sources(const fs::path& logs_dir) { + const std::pair wanted[] = { + {"tui", "logs/tui.log.tail.txt"}, + {"daemon", "logs/daemon.log.tail.txt"}, + }; + return collect_surface_runtime_log_sources( + logs_dir, wanted, sizeof(wanted) / sizeof(wanted[0])); +} + std::optional collect_recent_upgrade_log_bundle( const fs::path& logs_dir, std::chrono::hours window) { std::error_code ec; diff --git a/src/feedback/feedback_upload.hpp b/src/feedback/feedback_upload.hpp index fa0f85e2..f95c976c 100644 --- a/src/feedback/feedback_upload.hpp +++ b/src/feedback/feedback_upload.hpp @@ -103,12 +103,17 @@ std::optional latest_rotated_log_path( std::optional latest_desktop_log_path( const std::filesystem::path& logs_dir); -// daemon / desktop 反馈默认附带的运行时日志:desktop 壳日志 + daemon 日志, -// 各取最近一个滚动文件,缺失的静默跳过。升级日志不在这里 —— 它按窗口取多个, +// GUI/Desktop 反馈默认附带的运行时日志:Desktop 壳日志 + daemon 日志, +// 各取最近一个滚动文件,缺失时静默跳过。升级日志不在这里 —— 它按窗口取多个, // 走 collect_recent_upgrade_log_bundle。 std::vector collect_runtime_log_sources( const std::filesystem::path& logs_dir); +// TUI 反馈默认附带的运行时日志:TUI + daemon 日志。它刻意不附带 Desktop 壳日志, +// 因为反馈包只携带发起界面的表面日志。 +std::vector collect_tui_runtime_log_sources( + const std::filesystem::path& logs_dir); + // 找 logs_dir 下最近 window 内写过的全部 "upgrade--.log",按修改时间 // 从旧到新排成一个合并包(条目名固定 logs/upgrade.log.tail.txt)。窗口内一个都 // 没有返回 nullopt:更早的升级记录不做兜底,反馈里没有升级条目就意味着这三天 diff --git a/src/main.cpp b/src/main.cpp index 95e9bdc3..d129de66 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3048,7 +3048,13 @@ static void set_startup_terminal_title() { } static void initialize_logger_for_working_dir(const std::string& working_dir) { - Logger::instance().init(working_dir + "/acecode.log"); + const std::string logs_dir = get_logs_dir(); + Logger::instance().init_with_rotation(logs_dir, "tui", /*mirror_stderr=*/false); +#ifdef _WIN32 + _putenv_s("ACECODE_FTXUI_INPUT_TRACE_DIR", logs_dir.c_str()); +#else + setenv("ACECODE_FTXUI_INPUT_TRACE_DIR", logs_dir.c_str(), 1); +#endif Logger::instance().set_level(LogLevel::Dbg); LOG_INFO("=== acecode started, cwd=" + working_dir + " ==="); } diff --git a/src/tui/tui_init.cpp b/src/tui/tui_init.cpp index d275f32b..d92b1da5 100644 --- a/src/tui/tui_init.cpp +++ b/src/tui/tui_init.cpp @@ -94,7 +94,13 @@ void set_startup_terminal_title() { } void initialize_logger_for_working_dir(const std::string& working_dir) { - Logger::instance().init(working_dir + "/acecode.log"); + const std::string logs_dir = get_logs_dir(); + Logger::instance().init_with_rotation(logs_dir, "tui", /*mirror_stderr=*/false); +#ifdef _WIN32 + _putenv_s("ACECODE_FTXUI_INPUT_TRACE_DIR", logs_dir.c_str()); +#else + setenv("ACECODE_FTXUI_INPUT_TRACE_DIR", logs_dir.c_str(), 1); +#endif Logger::instance().set_level(LogLevel::Dbg); LOG_INFO("=== acecode started, cwd=" + working_dir + " ==="); } diff --git a/tests/feedback/feedback_upload_test.cpp b/tests/feedback/feedback_upload_test.cpp index fc0b4a48..6c8521c8 100644 --- a/tests/feedback/feedback_upload_test.cpp +++ b/tests/feedback/feedback_upload_test.cpp @@ -286,6 +286,56 @@ TEST(FeedbackUpload, BuildPackageIncludesDesktopAndDaemonLogs) { EXPECT_EQ(metadata["included_files"][2], "feedback.json"); } +TEST(FeedbackUpload, CollectTuiRuntimeLogSourcesIncludesTuiAndDaemonButNotDesktop) { + TempDir tmp("acecode_feedback_tui_sources"); + const fs::path logs = tmp.root / "logs"; + const auto older_tui = logs / "tui-2026-06-17.log"; + const auto newer_tui = logs / "tui-2026-06-18.log"; + write_text(older_tui, "older tui"); + write_text(newer_tui, "newer tui"); + write_text(logs / "desktop-2026-06-18.log", "unrelated desktop"); + write_text(logs / "daemon-2026-06-18.log", "daemon"); + const auto now = fs::file_time_type::clock::now(); + fs::last_write_time(older_tui, now - std::chrono::hours(2)); + fs::last_write_time(newer_tui, now - std::chrono::hours(1)); + + const auto tui_sources = acecode::feedback::collect_tui_runtime_log_sources(logs); + ASSERT_EQ(tui_sources.size(), 2u); + EXPECT_EQ(tui_sources[0].entry_name, "logs/tui.log.tail.txt"); + EXPECT_EQ(tui_sources[0].path.filename(), fs::path("tui-2026-06-18.log")); + EXPECT_EQ(tui_sources[1].entry_name, "logs/daemon.log.tail.txt"); + + const auto desktop_sources = acecode::feedback::collect_runtime_log_sources(logs); + ASSERT_EQ(desktop_sources.size(), 2u); + EXPECT_EQ(desktop_sources[0].entry_name, "logs/desktop.log.tail.txt"); + EXPECT_EQ(desktop_sources[1].entry_name, "logs/daemon.log.tail.txt"); + + const fs::path legacy_workspace_log = tmp.root / "workspace" / "acecode.log"; + write_text(legacy_workspace_log, "legacy workspace log"); + acecode::feedback::FeedbackPackageRequest req; + req.source = "tui"; + req.logs = tui_sources; + req.output_dir = tmp.root / "out"; + req.created_at = "2026-06-18T01:02:03Z"; + const auto package = acecode::feedback::build_feedback_package(req); + ASSERT_TRUE(package.ok) << package.error; + EXPECT_TRUE(zip_entry_exists(package.package_path, "logs/tui.log.tail.txt")); + EXPECT_TRUE(zip_entry_exists(package.package_path, "logs/daemon.log.tail.txt")); + EXPECT_FALSE(zip_entry_exists(package.package_path, "logs/desktop.log.tail.txt")); + EXPECT_FALSE(zip_entry_exists(package.package_path, "logs/acecode.log.tail.txt")); +} + +TEST(FeedbackUpload, CollectTuiRuntimeLogSourcesKeepsDaemonWhenTuiIsMissing) { + TempDir tmp("acecode_feedback_tui_missing"); + const fs::path logs = tmp.root / "logs"; + write_text(logs / "desktop-2026-06-18.log", "unrelated desktop"); + write_text(logs / "daemon-2026-06-18.log", "daemon"); + + const auto sources = acecode::feedback::collect_tui_runtime_log_sources(logs); + ASSERT_EQ(sources.size(), 1u); + EXPECT_EQ(sources[0].entry_name, "logs/daemon.log.tail.txt"); +} + TEST(FeedbackUpload, CollectRuntimeLogSourcesKeepsDaemonWhenDesktopIsMissing) { TempDir tmp("acecode_feedback_daemon_only"); const fs::path logs = tmp.root / "logs"; diff --git a/tests/utils/logger_test.cpp b/tests/utils/logger_test.cpp index c25a5964..b9defefd 100644 --- a/tests/utils/logger_test.cpp +++ b/tests/utils/logger_test.cpp @@ -128,6 +128,32 @@ TEST_F(LoggerRotationTest, InitWithRotationCreatesDirAndInitialFile) { EXPECT_EQ(matched, 1) << "应有且只有一个 daemon-{date}.log 被创建"; } +// 场景:TUI 使用与其它运行时表面相同的日期滚动命名,但保留不镜像 stderr 的 +// 终端运行语义。 +TEST_F(LoggerRotationTest, TuiRotationUsesTuiDatePrefixWithoutStderrMirror) { + auto logs_dir = tmp_dir_ / "logs"; + acecode::Logger::instance().init_with_rotation( + logs_dir.string(), "tui", /*mirror_stderr=*/false); + + std::ostringstream captured; + auto* old_buf = std::cerr.rdbuf(captured.rdbuf()); + LOG_INFO("tui-centralized-line"); + std::cerr.rdbuf(old_buf); + + int matched = 0; + for (const auto& entry : fs::directory_iterator(logs_dir)) { + const auto filename = entry.path().filename().string(); + if (filename.rfind("tui-", 0) == 0 && + filename.size() > 11 /* tui-YYYY-MM-DD.log */) { + ++matched; + EXPECT_NE(read_file(entry.path()).find("tui-centralized-line"), + std::string::npos); + } + } + EXPECT_EQ(matched, 1); + EXPECT_TRUE(captured.str().empty()); +} + // 场景:启动早期尚无主 logger 时建立静默 config sink;若调用方已有 sink, // 条件初始化不得把日志重定向到另一个文件。 TEST_F(LoggerRotationTest, ConditionalRotationInitializesOnlyWhenDisabled) { From f67c68e3b7744de6efe7751de1a30e72d844d4b8 Mon Sep 17 00:00:00 2001 From: tmoonlight Date: Sun, 20 Sep 2026 04:27:38 +0800 Subject: [PATCH 2/2] Fix UTF-8 paths and concurrent runtime log appends --- CMakeLists.txt | 1 + CONTEXT.md | 13 --- docs/adr/0002-feedback-origin-log-boundary.md | 5 ++ external/ftxui | 2 +- .../changes/centralize-tui-logs/design.md | 6 ++ .../specs/tui-runtime-logging/spec.md | 14 ++++ openspec/changes/centralize-tui-logs/tasks.md | 9 ++ ports/ftxui/portfile.cmake | 3 +- ports/ftxui/vcpkg.json | 2 +- src/utils/append_file.cpp | 71 ++++++++++++++++ src/utils/append_file.hpp | 27 ++++++ src/utils/logger.hpp | 25 +++--- tests/CMakeLists.txt | 20 +++++ tests/scripts/runtime_log_append_test.py | 84 +++++++++++++++++++ tests/utils/helpers/runtime_log_fixture.cpp | 67 +++++++++++++++ tests/utils/logger_test.cpp | 30 ++++++- 16 files changed, 349 insertions(+), 30 deletions(-) delete mode 100644 CONTEXT.md create mode 100644 src/utils/append_file.cpp create mode 100644 src/utils/append_file.hpp create mode 100644 tests/scripts/runtime_log_append_test.py create mode 100644 tests/utils/helpers/runtime_log_fixture.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 04cef084..35ac8d60 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,6 +198,7 @@ set(ACECODE_NATIVE_BRIDGE_SUPPORT_SOURCES ${CMAKE_SOURCE_DIR}/src/desktop/startup_progress.cpp ${CMAKE_SOURCE_DIR}/src/desktop/strings.cpp ${CMAKE_SOURCE_DIR}/src/desktop/workspace_registry.cpp + ${CMAKE_SOURCE_DIR}/src/utils/append_file.cpp ${CMAKE_SOURCE_DIR}/src/utils/clipboard.cpp ${CMAKE_SOURCE_DIR}/src/utils/cwd_hash.cpp ${CMAKE_SOURCE_DIR}/src/utils/encoding.cpp diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index 1233a7b6..00000000 --- a/CONTEXT.md +++ /dev/null @@ -1,13 +0,0 @@ -# ACECode - -ACECode is a local AI coding agent with terminal, desktop, daemon, and headless runtime surfaces. This context records product terms that distinguish those surfaces and their diagnostic artifacts. - -## Language - -**Feedback origin**: -The runtime surface that initiates a diagnostic feedback package: `tui` for the terminal command and `desktop` for the GUI/Desktop endpoint. -_Avoid_: Feedback client, UI type - -**Surface log**: -The dated runtime log produced by one ACECode surface, named with that surface's prefix in the effective data directory's `logs` subdirectory. -_Avoid_: Shared UI log, workspace log diff --git a/docs/adr/0002-feedback-origin-log-boundary.md b/docs/adr/0002-feedback-origin-log-boundary.md index 264c72e4..76198b4e 100644 --- a/docs/adr/0002-feedback-origin-log-boundary.md +++ b/docs/adr/0002-feedback-origin-log-boundary.md @@ -1,3 +1,8 @@ # Feedback packages include only their originating surface log TUI feedback includes the latest TUI surface log, while GUI/Desktop feedback includes the latest Desktop surface log; neither includes the other surface's log. This keeps diagnostic attachments relevant to the reporting surface and avoids unnecessarily sending diagnostics from an unrelated interface. + +## 术语 + +- **Feedback origin(反馈来源)**:发起诊断反馈包的运行界面。终端命令使用 `tui`,GUI/Desktop 接口使用 `desktop`;避免用 Feedback client 或 UI type 混淆运行界面与客户端。 +- **Surface log(界面运行日志)**:某个运行界面在有效数据目录的 `logs` 子目录中生成的按日期日志,文件名使用该界面的前缀;避免称作 Shared UI log 或 Workspace log。 diff --git a/external/ftxui b/external/ftxui index ca7fbc06..62e7471b 160000 --- a/external/ftxui +++ b/external/ftxui @@ -1 +1 @@ -Subproject commit ca7fbc06e82aee78c3f96c1a169366aba972c379 +Subproject commit 62e7471b1e4155a33ee9e3caa0921afc8f7326e6 diff --git a/openspec/changes/centralize-tui-logs/design.md b/openspec/changes/centralize-tui-logs/design.md index 24e2e568..1c3d3b3c 100644 --- a/openspec/changes/centralize-tui-logs/design.md +++ b/openspec/changes/centralize-tui-logs/design.md @@ -74,6 +74,12 @@ Update the user manual's FAQ and MCP troubleshooting text to use `<数据目录> ## Risks / Trade-offs +### PR 审核补充:Unicode 路径与多进程写入 + +Windows 创建目录与打开日志统一从 UTF-8 转换到原生路径,避免首次启动在中文数据目录创建乱码目录。集中日志允许多个工作区的 TUI 同时写入,因此不能用进程内互斥锁保护 CRT 的 seek-to-end + write:主日志使用独立的低层追加文件模块,Windows 以 `FILE_APPEND_DATA` 打开并整条 `WriteFile`,POSIX 以 `O_APPEND` 打开并整条 `write`。FTXUI 在其自身依赖边界内使用同样的系统语义,保持追踪记录格式。拒绝相对追踪目录;打开失败时维持静默 best-effort,不写工作区备用文件。 + +回归验证包含独立进程同时写同一文件、中文路径首次创建及无法打开的目标。操作系统报告短写或磁盘错误时不把残余片段另行追加,以免把一条记录拆散穿插到其他进程记录中;这些错误仍按既有日志 best-effort 语义处理。 + - **Feedback archive name change:** consumers that recognize `logs/acecode.log.tail.txt` will no longer see a TUI legacy entry. The old file was workspace-specific and is no longer a valid runtime source; a distinct `logs/tui.log.tail.txt` accurately identifies the new source. The high-volume FTXUI input trace remains a local diagnostic artifact and is not attached by either feedback origin. - **Startup ordering:** TUI logger initialization remains before normal configuration loading, as it is today. `get_logs_dir()` only uses existing data-directory resolution and does not require loaded application configuration. - **Old docs and external scripts:** workspace-local `acecode.log` may be referenced externally. The selected migration policy intentionally preserves old files but does not maintain an active compatibility copy, preventing further workspace pollution. diff --git a/openspec/changes/centralize-tui-logs/specs/tui-runtime-logging/spec.md b/openspec/changes/centralize-tui-logs/specs/tui-runtime-logging/spec.md index fb058742..50854786 100644 --- a/openspec/changes/centralize-tui-logs/specs/tui-runtime-logging/spec.md +++ b/openspec/changes/centralize-tui-logs/specs/tui-runtime-logging/spec.md @@ -86,6 +86,20 @@ Failure to find a rotated TUI log MUST remain non-fatal and MUST NOT prevent col - **THEN** feedback packaging continues without `logs/tui.log.tail.txt` - **AND THEN** other available daemon or upgrade log sources continue to be collected +### Requirement: 集中日志支持 Unicode 路径和多进程追加 + +主日志和 FTXUI 输入追踪 MUST 正确创建中文数据目录下的日志目录。多个进程向同一日期文件写入时 MUST 使用操作系统原子追加定位,不能因并发 seek/write 覆盖其他进程已写记录。追踪目录缺失、为相对路径或不可写时 MUST 静默跳过,不能回退到工作区文件。 + +#### Scenario: 多工作区同时写日志 + +- **WHEN** 多个 TUI 进程同时向相同数据目录写入正常长度的日志或输入追踪记录 +- **THEN** 所有成功写入的完整记录各出现一次,互不覆盖 + +#### Scenario: 首次创建中文日志目录 + +- **WHEN** 有效数据目录含中文且 logs 子目录尚不存在 +- **THEN** 初始化创建正确 Unicode 路径并写入对应日期日志 + ### Requirement: User documentation describes the unified runtime log location User-facing documentation SHALL describe the interactive TUI log as `/logs/tui-YYYY-MM-DD.log` and direct TUI troubleshooting, including MCP connection troubleshooting, to the centralized runtime logs directory. It MUST NOT describe normal TUI runtime logging as writing `/acecode.log`. diff --git a/openspec/changes/centralize-tui-logs/tasks.md b/openspec/changes/centralize-tui-logs/tasks.md index 56298b6e..52467485 100644 --- a/openspec/changes/centralize-tui-logs/tasks.md +++ b/openspec/changes/centralize-tui-logs/tasks.md @@ -30,3 +30,12 @@ - [ ] 5.1 Run the focused logger, FTXUI trace-path, and feedback native tests. - [ ] 5.2 Configure and build an input-trace-enabled TUI executable with the bumped FTXUI port, then verify normal-workspace and `--worktree` runs leave no trace-created `acecode.log`. - [x] 5.3 Review the documentation diff and verify normal-TUI and FTXUI input-trace documentation no longer directs users to workspace-local `acecode.log`. + +## 6. PR 审核修复 + +- [x] 6.1 修复 Windows UTF-8 日志目录首次创建,覆盖中文用户目录及重定向目录。 +- [x] 6.2 主日志与 FTXUI 输入追踪使用操作系统追加语义,避免多进程 seek/write 竞争覆盖记录。 +- [x] 6.3 增加并运行中文路径、多进程完整记录、追踪路径与不可写目标的独立回归,记录验证边界。 +- [x] 6.4 将 PR 新增根目录 CONTEXT.md 的术语移入已有反馈 ADR,保持根目录规范;现存同名引用均指 OpenCode 竞品文档。 + +审核验证:Windows MSVC 独立 fixture 的 4 项 Python 回归和 10 项 LoggerRotationTest 通过;主日志、低层追加和 FTXUI 输入追踪各 4 进程 × 2000 条,均收到 8000 条完整唯一记录。中文新目录、追踪本地午夜、空/相对/不可打开目录均通过;开启 `ACECODE_TUI_INPUT_TRACE=1` 的真实 FTXUI app.cpp 定向编译通过。完整交互式 TUI 与非 Windows 实机验证仍由 5.2 和后续构建覆盖,不将 fixture 测试记作完整 TUI 运行。 diff --git a/ports/ftxui/portfile.cmake b/ports/ftxui/portfile.cmake index 6c55b365..e6d051ff 100644 --- a/ports/ftxui/portfile.cmake +++ b/ports/ftxui/portfile.cmake @@ -6,8 +6,9 @@ # # 因此:**每次 external/ftxui 的 gitlink 变更,必须同步 bump vcpkg.json 的 # port-version**,否则改动不会生效。当前对应子模块 commit: -# f98588b4 (v7.0.3-22,含 synchronized output + opt-in mouse hover motion) +# 62e7471b (含 synchronized output + opt-in mouse hover motion) # —— 输入追踪日志路径由 ACECODE_FTXUI_INPUT_TRACE_DIR 运行时指定。 +# —— 原子追加输入追踪记录,避免多个 TUI 进程互相覆盖。 # —— hover-motion 补丁(link-hover-tooltip 依赖无按键 Mouse::Moved 上报, # DEC mode 1003),提供 ftxui::App::EnableMouseHoverMotion()。 vcpkg_check_linkage(ONLY_STATIC_LIBRARY) diff --git a/ports/ftxui/vcpkg.json b/ports/ftxui/vcpkg.json index e35e81fe..7e8e30de 100644 --- a/ports/ftxui/vcpkg.json +++ b/ports/ftxui/vcpkg.json @@ -1,7 +1,7 @@ { "name": "ftxui", "version-semver": "7.0.3", - "port-version": 7, + "port-version": 8, "description": "C++ Functional Terminal User Interface (shaohaozhi286 fork; adds ACECode TUI selection, input trace, Kitty keyboard lifecycle, and synchronized output (DEC mode 2026) patches — see external/ftxui/ACECODE_PATCHES.md)", "homepage": "https://github.com/shaohaozhi286/FTXUI", "license": "MIT", diff --git a/src/utils/append_file.cpp b/src/utils/append_file.cpp new file mode 100644 index 00000000..b0fb67c5 --- /dev/null +++ b/src/utils/append_file.cpp @@ -0,0 +1,71 @@ +#include "append_file.hpp" + +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#include +#include +#endif + +namespace acecode { + +AppendFile::~AppendFile() { close(); } + +bool AppendFile::open(const std::filesystem::path& path) { + close(); +#ifdef _WIN32 + // Do not request GENERIC_WRITE: CRT append streams seek before WriteFile, + // allowing another process to write into the same offset between the two. + HANDLE file = CreateFileW(path.c_str(), FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + handle_ = reinterpret_cast(file); +#else + handle_ = ::open(path.c_str(), O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0666); +#endif + return is_open(); +} + +void AppendFile::close() { + if (!is_open()) return; +#ifdef _WIN32 + CloseHandle(reinterpret_cast(handle_)); +#else + ::close(static_cast(handle_)); +#endif + handle_ = -1; +} + +bool AppendFile::append(std::string_view record) { + if (!is_open()) return false; + if (record.empty()) return true; +#ifdef _WIN32 + if (record.size() > (std::numeric_limits::max)()) return false; + DWORD written = 0; + return WriteFile(reinterpret_cast(handle_), record.data(), + static_cast(record.size()), &written, nullptr) != FALSE && + written == record.size(); +#else + if (record.size() > static_cast((std::numeric_limits::max)())) { + return false; + } + ssize_t written; + do { + written = ::write(static_cast(handle_), record.data(), record.size()); + } while (written < 0 && errno == EINTR); + // A short write (e.g. disk full) must not become separately appended pieces + // that can be interleaved with another process's complete log record. + return written >= 0 && static_cast(written) == record.size(); +#endif +} + +} // namespace acecode diff --git a/src/utils/append_file.hpp b/src/utils/append_file.hpp new file mode 100644 index 00000000..c471937a --- /dev/null +++ b/src/utils/append_file.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +namespace acecode { + +// A file sink with OS append positioning across independent processes. Callers +// serialize access to this object and submit each complete record in one call. +class AppendFile { +public: + AppendFile() = default; + ~AppendFile(); + AppendFile(const AppendFile&) = delete; + AppendFile& operator=(const AppendFile&) = delete; + + bool open(const std::filesystem::path& path); + void close(); + bool is_open() const { return handle_ != -1; } + bool append(std::string_view record); + +private: + std::intptr_t handle_ = -1; +}; + +} // namespace acecode diff --git a/src/utils/logger.hpp b/src/utils/logger.hpp index 6d0faf77..04a1c04f 100644 --- a/src/utils/logger.hpp +++ b/src/utils/logger.hpp @@ -21,6 +21,8 @@ #include #include +#include "append_file.hpp" + namespace acecode { enum class LogLevel { Dbg = 0, Info = 1, Warn = 2, Err = 3 }; @@ -32,8 +34,7 @@ class Logger { return inst; } - // TUI / 单文件模式: 写入指定文件,不滚动,不镜像 stderr。 - // 与原 v1 行为兼容,保持已有调用点(main.cpp 写 acecode.log)零改动。 + // 兼容单文件模式:写入指定文件,不滚动,不镜像 stderr。 void init(const std::string& log_file) { std::lock_guard lk(mu_); if (ofs_.is_open()) ofs_.close(); @@ -42,11 +43,11 @@ class Logger { rotation_dir_.clear(); rotation_base_.clear(); last_open_date_.clear(); - ofs_.open(path_from_utf8_(log_file), std::ios::out | std::ios::app); + ofs_.open(path_from_utf8_(log_file)); enabled_ = ofs_.is_open(); } - // daemon 模式: 写入 dir/-.log,跨本地午夜 + // 滚动模式:写入 dir/-.log,跨本地午夜 // 自动滚动到新日期文件。mirror_stderr=true 时每条日志同时写 stderr // (foreground 模式)。dir 不存在会被创建。 void init_with_rotation(const std::string& dir, @@ -67,7 +68,10 @@ class Logger { return enabled_; } - void set_level(LogLevel level) { level_ = level; } + void set_level(LogLevel level) { + std::lock_guard lk(mu_); + level_ = level; + } // 测试专用: 注入一个返回 "YYYY-MM-DD" 字符串的 callable,用于强制 // 触发跨日滚动而无需等真实午夜。传空 std::function 还原为真实时钟。 @@ -77,8 +81,8 @@ class Logger { } void log(LogLevel level, const char* file, int line, const std::string& msg) { - if (!enabled_ || level < level_) return; std::lock_guard lk(mu_); + if (!enabled_ || level < level_) return; auto now = std::chrono::system_clock::now(); auto time = std::chrono::system_clock::to_time_t(now); @@ -126,8 +130,7 @@ class Logger { const std::string s = line_oss.str(); if (ofs_.is_open()) { - ofs_ << s; - ofs_.flush(); + ofs_.append(s); } if (mirror_stderr_) { std::cerr << s; @@ -149,7 +152,7 @@ class Logger { rotation_dir_ = dir; rotation_base_ = base_name; std::error_code ec; - std::filesystem::create_directories(dir, ec); + std::filesystem::create_directories(path_from_utf8_(dir), ec); open_rotated_locked_(current_date_string_()); } @@ -196,12 +199,12 @@ class Logger { if (ofs_.is_open()) ofs_.close(); auto path = path_from_utf8_(rotation_dir_) / (rotation_base_ + "-" + date + ".log"); - ofs_.open(path, std::ios::out | std::ios::app); + ofs_.open(path); last_open_date_ = date; enabled_ = ofs_.is_open(); } - std::ofstream ofs_; + AppendFile ofs_; std::mutex mu_; LogLevel level_ = LogLevel::Dbg; bool enabled_ = false; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 116bb00b..ceda682c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -63,6 +63,26 @@ file(GLOB_RECURSE ACECODE_TEST_SOURCES CONFIGURE_DEPENDS add_executable(acecode_unit_tests ${ACECODE_TEST_SOURCES}) +add_executable(acecode_runtime_log_fixture + ${CMAKE_CURRENT_SOURCE_DIR}/utils/helpers/runtime_log_fixture.cpp + ${CMAKE_SOURCE_DIR}/src/utils/append_file.cpp) +target_include_directories(acecode_runtime_log_fixture PRIVATE + ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/external/ftxui/src) +if(MSVC) + set_target_properties(acecode_runtime_log_fixture PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +elseif(MINGW) + target_link_options(acecode_runtime_log_fixture PRIVATE -municode) +endif() +add_dependencies(acecode_unit_tests acecode_runtime_log_fixture) +if(Python3_Interpreter_FOUND) + add_test(NAME runtime_log_append + COMMAND ${Python3_EXECUTABLE} -B + ${CMAKE_CURRENT_SOURCE_DIR}/scripts/runtime_log_append_test.py + $) + set_tests_properties(runtime_log_append PROPERTIES LABELS "unit" TIMEOUT 120) +endif() + add_executable(acecode_upgrade_version_fixture ${CMAKE_CURRENT_SOURCE_DIR}/upgrade/helpers/version_fixture.cpp) if(MSVC) diff --git a/tests/scripts/runtime_log_append_test.py b/tests/scripts/runtime_log_append_test.py new file mode 100644 index 00000000..4135bb84 --- /dev/null +++ b/tests/scripts/runtime_log_append_test.py @@ -0,0 +1,84 @@ +"""Exercise the production append and trace writers in independent processes.""" +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import unittest + +FIXTURE = Path(sys.argv.pop(1)).resolve() +CREATE_FLAGS = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0 + + +class RuntimeLogAppendTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="acecode-runtime-logs-") + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + + def invoke(self, *args): + return subprocess.run([str(FIXTURE), *map(str, args)], cwd=self.root, + capture_output=True, text=True, timeout=30, + creationflags=CREATE_FLAGS, check=True) + + def assert_records(self, text, processes, count): + lines = text.splitlines() + records = [re.search(r"record=(\d+):(\d+) (x{300})$", line) for line in lines] + self.assertEqual(len(lines), processes * count) + self.assertTrue(all(records), "truncated or interleaved log record") + self.assertEqual({(int(m[1]), int(m[2])) for m in records}, + {(p, i) for p in range(processes) for i in range(count)}) + + def test_missing_unicode_directory_and_separate_trace(self): + target = self.root / "中文数据" / "logs" + self.invoke("logger", target, 0, 1) + self.invoke("trace", target, 0, 1) + self.assertTrue(target.is_dir()) + primary = list(target.glob("tui-????-??-??.log")) + trace = list(target.glob("tui-input-trace-????-??-??.log")) + self.assertEqual(len(primary), 1) + self.assertEqual(len(trace), 1) + self.assert_records(primary[0].read_text(), 1, 1) + self.assert_records(trace[0].read_text(), 1, 1) + self.assertFalse((self.root / "acecode.log").exists()) + + def test_processes_preserve_every_complete_record(self): + for mode in ("raw", "logger", "trace"): + with self.subTest(mode=mode): + target = self.root / mode + processes, count = 4, 2000 + children = [subprocess.Popen( + [str(FIXTURE), mode, str(target), str(p), str(count)], + cwd=self.root, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + creationflags=CREATE_FLAGS) for p in range(processes)] + try: + for child in children: + self.assertEqual(child.wait(timeout=30), 0) + finally: + for child in children: + if child.poll() is None: + child.kill() + child.wait(timeout=5) + paths = [target] if mode == "raw" else list(target.glob("*.log")) + self.assertEqual(len(paths), 1) + self.assert_records(paths[0].read_text(), processes, count) + + def test_trace_uses_local_date_across_midnight(self): + result = self.invoke("trace-path", self.root) + self.assertEqual(result.stdout.splitlines(), [ + "tui-input-trace-2026-09-20.log", "tui-input-trace-2026-09-21.log"]) + + def test_trace_refuses_unconfigured_relative_and_unopenable_paths(self): + blocked = self.root / "regular-file" + blocked.write_text("preserved") + for target in ("", "relative", blocked / "logs"): + with self.subTest(target=target): + self.invoke("trace-reject", target) + self.assertEqual(blocked.read_text(), "preserved") + self.assertFalse((self.root / "relative").exists()) + self.assertFalse((self.root / "acecode.log").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/utils/helpers/runtime_log_fixture.cpp b/tests/utils/helpers/runtime_log_fixture.cpp new file mode 100644 index 00000000..241088b4 --- /dev/null +++ b/tests/utils/helpers/runtime_log_fixture.cpp @@ -0,0 +1,67 @@ +#include "utils/logger.hpp" +#include "ftxui/component/acecode_input_trace.hpp" + +#include +#include +#include +#include +#include + +int run(const std::vector& args) { + if (args.size() < 3) return 2; + const auto directory = std::filesystem::u8path(args[2]); + if (args[1] == "trace-path") { + std::tm local{}; + local.tm_year = 126; + local.tm_mon = 8; + local.tm_mday = 20; + local.tm_hour = 23; + local.tm_min = 59; + local.tm_sec = 59; + local.tm_isdst = -1; + const auto first = std::chrono::system_clock::from_time_t(std::mktime(&local)); + for (const auto now : {first, first + std::chrono::seconds(1)}) { + const auto path = ftxui::detail::AcecodeInputTracePath(args[2].c_str(), now); + if (!path) return 3; + std::cout << path->filename().u8string() << '\n'; + } + return 0; + } + if (args[1] == "trace-reject") { + return ftxui::detail::WriteAcecodeInputTrace(args[2].c_str(), "rejected") ? 4 : 0; + } + if (args.size() != 5) return 2; + const int count = std::stoi(args[4]); + acecode::AppendFile raw; + if (args[1] == "raw") { + if (!raw.open(directory)) return 5; + } else if (args[1] == "logger") { + acecode::Logger::instance().init_with_rotation(args[2], "tui", false); + } else if (args[1] != "trace") { + return 2; + } + for (int i = 0; i < count; ++i) { + const std::string record = "record=" + args[3] + ":" + std::to_string(i) + + " " + std::string(300, 'x'); + if (args[1] == "raw") { + if (!raw.append(record + "\n")) return 6; + } else if (args[1] == "trace") { + if (!ftxui::detail::WriteAcecodeInputTrace(args[2].c_str(), record)) return 7; + } else { + LOG_INFO(record); + } + } + return 0; +} + +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + std::vector args; + for (int i = 0; i < argc; ++i) args.push_back(std::filesystem::path(argv[i]).u8string()); + return run(args); +} +#else +int main(int argc, char** argv) { + return run(std::vector(argv, argv + argc)); +} +#endif diff --git a/tests/utils/logger_test.cpp b/tests/utils/logger_test.cpp index b9defefd..674147ec 100644 --- a/tests/utils/logger_test.cpp +++ b/tests/utils/logger_test.cpp @@ -1,6 +1,6 @@ // 覆盖 src/utils/logger.hpp 的 daemon 模式滚动日志(spec Section 12)。 // 单元测试这层主要验证三件事: -// 1. 老 init(file) API 行为不变 — TUI 仍然写 acecode.log,不滚动,不镜像 stderr +// 1. 老 init(file) API 行为不变 — 指定单文件,不滚动,不镜像 stderr // 2. init_with_rotation() 创建 logs/ 目录并写到 daemon-{今日}.log // 3. 跨日滚动: 注入假时钟,把"今天"推到下一天,下一条日志要落到新文件 // 4. foreground 模式: 每条日志同时落文件 + stderr @@ -62,8 +62,7 @@ class LoggerRotationTest : public ::testing::Test { } // namespace -// 场景: 老 API init(file) 维持 TUI 行为 — 写到指定单文件,不创建带日期的文件, -// 也不输出 stderr。这是兼容性硬保证(main.cpp:770 一直这么调)。 +// 场景:老 API init(file) 写到指定单文件,不创建带日期的文件,也不输出 stderr。 TEST_F(LoggerRotationTest, SingleFileModeWritesToTargetFileOnly) { auto log_path = tmp_dir_ / "acecode.log"; acecode::Logger::instance().init(log_path.string()); @@ -154,6 +153,31 @@ TEST_F(LoggerRotationTest, TuiRotationUsesTuiDatePrefixWithoutStderrMirror) { EXPECT_TRUE(captured.str().empty()); } +TEST_F(LoggerRotationTest, RotationCreatesPreviouslyMissingUtf8Directory) { + const auto logs_dir = tmp_dir_ / fs::u8path(u8"中文数据") / "logs"; + ASSERT_FALSE(fs::exists(logs_dir)); + acecode::Logger::instance().init_with_rotation(logs_dir.u8string(), "tui", false); + LOG_INFO("utf8-directory-record"); + ASSERT_TRUE(fs::is_directory(logs_dir)); + int found = 0; + for (const auto& entry : fs::directory_iterator(logs_dir)) { + if (read_file(entry.path()).find("utf8-directory-record") != std::string::npos) { + ++found; + } + } + EXPECT_EQ(found, 1); +} + +TEST_F(LoggerRotationTest, UnopenableLogDirectoryDoesNotCreateWorkspaceFallback) { + const auto blocked = tmp_dir_ / "regular-file"; + { std::ofstream out(blocked); out << "preserved"; } + acecode::Logger::instance().init_with_rotation( + (blocked / "logs").u8string(), "tui", false); + EXPECT_NO_THROW(LOG_INFO("discarded-unopenable-record")); + EXPECT_EQ(read_file(blocked), "preserved"); + EXPECT_FALSE(fs::exists(tmp_dir_ / "acecode.log")); +} + // 场景:启动早期尚无主 logger 时建立静默 config sink;若调用方已有 sink, // 条件初始化不得把日志重定向到另一个文件。 TEST_F(LoggerRotationTest, ConditionalRotationInitializesOnlyWhenDisabled) {