Skip to content

fix(windows): 收敛启动和开发产物路径#48

Merged
appergb merged 2 commits into
Open-Less:mainfrom
Cooper-X-Oak:codex/windows-startup-artifacts-pr
Apr 30, 2026
Merged

fix(windows): 收敛启动和开发产物路径#48
appergb merged 2 commits into
Open-Less:mainfrom
Cooper-X-Oak:codex/windows-startup-artifacts-pr

Conversation

@Cooper-X-Oak
Copy link
Copy Markdown
Contributor

@Cooper-X-Oak Cooper-X-Oak commented Apr 30, 2026

摘要

关联 fork 验证:Cooper-X-Oak#12

本 PR 是从 fork/dev 已验证批次拆出的第二个最小 upstream 维护项:修复 Windows dev artifact 启动路径和启动首屏空壳/白屏体验。它不包含热键 core、ASR、插入 fallback、权限状态等其他 Windows 真机修复。

fork/dev 先行验证

修复 / 新增 / 改进

  • Windows 启动不再在权限探测完成前显示空白窗口;前端先显示轻量启动壳,再进入主界面。
  • Tauri setup 不再无条件过早 show 主窗口;dev/smoke 可通过 OPENLESS_SHOW_MAIN_ON_START=1 显式唤起主窗口。
  • tray icon 缺失时不再 unwrap panic,而是记录 warning 并继续启动。
  • windows-build-gnu.ps1 将 dev exe 和 WebView2Loader.dll 复制到仓库内 .artifacts/windows-gnu/dev,bundle 复制到 .artifacts/windows-gnu/release
  • 默认清理 no-space scratch build root,避免 %TEMP%\openless-windows-gnu 长期占用磁盘。
  • 新增 windows-open-dev.ps1windows-runtime-smoke.ps1,用于快速唤起和启动链路回归。

兼容

  • 不包含:热键 core、ASR、插入 fallback、麦克风权限、设置页凭据保存。
  • 对现有用户 / 本地环境 / 构建流程的影响:Windows GNU 本地构建产物位置从 scratch target 改为仓库 .artifacts.artifacts/ 已加入 app 级 .gitignore

测试计划

  • 命令:PowerShell Parser 检查 windows-build-gnu.ps1windows-open-dev.ps1windows-runtime-smoke.ps1
  • 结果:语法通过。
  • 命令:openless-all/app/scripts/windows-build-gnu.ps1
  • 结果:Windows GNU release/msi/nsis 构建通过;产物写入 .artifacts/windows-gnu
  • 命令:检查 .artifacts/windows-gnu/dev/openless.exeWebView2Loader.dll
  • 结果:两个文件均存在。
  • 命令:openless-all/app/scripts/windows-runtime-smoke.ps1
  • 结果:进程响应,窗口标题为 OpenLess,hotkey listener 日志出现。
  • 命令:openless-all/app/scripts/windows-open-dev.ps1
  • 结果:OpenLess started and brought to foreground.

Summary by Sourcery

Align Windows startup behavior and build artifacts layout, and add helper scripts for Windows dev and runtime smoke verification.

New Features:

  • Show a lightweight startup shell UI while permissions are being checked instead of presenting a blank screen.
  • Introduce a windows runtime smoke script to automate basic process, log, and credential checks.
  • Add a Windows dev helper script to launch the local GNU build and bring the main window to the foreground.

Bug Fixes:

  • Prevent the app from blocking initial Windows startup on permission probing, avoiding an empty first screen.
  • Avoid panicking when the tray icon cannot be created by gracefully skipping tray setup and logging a warning.

Enhancements:

  • Gate showing the main window on an explicit OPENLESS_SHOW_MAIN_ON_START environment flag to better control startup behavior across platforms.

Build:

  • Change the Windows GNU build script to place dev and bundled artifacts under the repository .artifacts/windows-gnu tree and copy WebView2Loader.dll beside the dev executable.
  • Add automatic cleanup of the temporary no-space mirror build directory by default, with an option to keep it for debugging.

Tests:

  • Add a scripted Windows runtime smoke check to validate startup responsiveness and hotkey listener installation.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 30, 2026

Reviewer's Guide

Adjusts Windows build artifacts layout, refines Tauri startup behavior to avoid premature/blank main window on Windows, hardens tray icon creation, and adds Windows helper scripts for dev launch and runtime smoke tests.

Sequence diagram for updated Windows startup and main window behavior

sequenceDiagram
    actor User
    participant OS_Windows
    participant OpenLess_Exe
    participant Tauri_Core
    participant Rust_App as Rust_app_run
    participant React_App as React_App_component

    User->>OS_Windows: launch openless.exe
    OS_Windows->>OpenLess_Exe: create process
    OpenLess_Exe->>Tauri_Core: initialize Tauri runtime
    Tauri_Core->>Rust_App: run()

    Rust_App->>Rust_App: setup_tray_icon
    alt default_window_icon available
        Rust_App->>Rust_App: TrayIconBuilder::with_id("main-tray")
        Rust_App->>Rust_App: .on_menu_event(toggle => show_main_window)
        Rust_App->>Rust_App: .on_tray_icon_event(click => show_main_window)
    else missing_icon
        Rust_App->>Rust_App: log::warn([startup] default window icon missing)
    end

    Rust_App->>Rust_App: coordinator.bind_app
    Rust_App->>Rust_App: coordinator.start_hotkey_listener

    alt env.OPENLESS_SHOW_MAIN_ON_START == "1"
        Rust_App->>Rust_App: show_main_window(app.handle())
    else no_env_flag
        Rust_App-->>Tauri_Core: do not show main window yet
    end

    Tauri_Core->>React_App: mount App(isCapsule=false)
    React_App->>React_App: detectOS() => win or other

    alt Windows (os == "win")
        React_App->>React_App: gate = 'ready'
    else macOS or others
        React_App->>React_App: gate = 'checking'
    end

    React_App->>React_App: useEffect(show_main_window_on_mount)
    React_App->>React_App: requestAnimationFrame
    React_App->>React_App: import @tauri-apps/api/window
    React_App->>@tauri-apps/api/window: getCurrentWindow().show()

    alt non-Windows (os != "win")
        React_App->>React_App: useEffect(permissions_check)
        React_App->>React_App: checkAccessibilityPermission()
        React_App->>React_App: checkMicrophonePermission()
        React_App->>React_App: setGate('ready') when done
    end

    alt gate == 'checking'
        React_App-->>User: render StartupShell
    else gate == 'ready'
        React_App-->>User: render main UI
    end
Loading

Sequence diagram for windows-open-dev.ps1 dev launch and foregrounding

sequenceDiagram
    actor Dev as Developer
    participant PS as PowerShell
    participant Script as windows_open_dev_ps1
    participant OS as Windows_OS
    participant OpenLess_Exe as openless_executable

    Dev->>PS: run windows-open-dev.ps1
    PS->>Script: invoke with optional ExePath

    alt ExePath not provided
        Script->>Script: resolve appRoot from PSScriptRoot
        Script->>Script: ExePath = .artifacts\windows-gnu\dev\openless.exe
    end

    Script->>Script: verify ExePath exists
    Script->>Script: verify WebView2Loader.dll beside ExePath

    Script->>Script: Get-Process openless
    alt existing process with MainWindowHandle
        Script->>Script: Show-OpenLessWindow(existing)
        Script-->>Dev: OpenLess is already running - brought to foreground
        Script-->>PS: exit 0
    else no existing window
        Script->>Script: set env.PATH for toolchain
        Script->>Script: set env.OPENLESS_SHOW_MAIN_ON_START = "1"
        Script->>OS: Start-Process ExePath (working directory = exe folder)
        Script->>Script: remove env.OPENLESS_SHOW_MAIN_ON_START
    end

    loop until deadline or window visible
        Script->>OS: Get-Process by id
        alt process has MainWindowHandle
            Script->>Script: Show-OpenLessWindow(process)
            Script-->>Dev: OpenLess started and brought to foreground
            Script-->>PS: exit 0
        else still headless
            Script->>Script: sleep 250ms
        end
    end

    alt deadline reached and no window
        Script-->>Dev: throw "no main window visible within 10 seconds"
    end
Loading

Flow diagram for updated Windows GNU build artifacts pipeline

flowchart TD
    A_start["Start windows-build-gnu.ps1"] --> B_params["Parse params: MirrorRoot, ArtifactsRoot, KeepMirror"]
    B_params --> C_setAppRoot["Resolve appRoot from script root"]
    C_setAppRoot --> D_checkArtifactsRoot{"ArtifactsRoot empty?"}
    D_checkArtifactsRoot -->|Yes| E_defaultArtifacts["ArtifactsRoot = appRoot/.artifacts/windows-gnu"]
    D_checkArtifactsRoot -->|No| F_useProvidedArtifacts["Use provided ArtifactsRoot"]

    E_defaultArtifacts --> G_checkSpaces
    F_useProvidedArtifacts --> G_checkSpaces["Does appRoot contain spaces?"]

    G_checkSpaces -->|Yes| H_mirror["Create MirrorRoot, robocopy appRoot -> MirrorRoot (exclude .artifacts, node_modules, dist, target)"]
    H_mirror --> I_setBuildRootMirror["buildRoot = MirrorRoot; usedMirror = true"]
    G_checkSpaces -->|No| J_setBuildRoot["buildRoot = appRoot; usedMirror = false"]

    I_setBuildRootMirror --> K_env["Set PATH, RUSTUP_TOOLCHAIN, CARGO_BUILD_TARGET"]
    J_setBuildRoot --> K_env

    K_env --> L_npmDeps{"node_modules exists?"}
    L_npmDeps -->|No| M_npmCI["npm ci"]
    L_npmDeps -->|Yes| N_skipNpmCI["Skip npm ci"]

    M_npmCI --> O_buildDev
    N_skipNpmCI --> O_buildDev["npm run tauri build -- --target x86_64-pc-windows-gnu --no-bundle"]

    O_buildDev --> P_releaseRoot["releaseRoot = buildRoot/src-tauri/target/x86_64-pc-windows-gnu/release"]
    P_releaseRoot --> Q_artifactDevRoot["artifactDevRoot = ArtifactsRoot/dev; mkdir"]
    Q_artifactDevRoot --> R_copyExeDev["Copy openless.exe -> artifactDevRoot/openless.exe"]
    R_copyExeDev --> S_resolveWebView2["Resolve-WebView2Loader() search under CARGO_HOME/registry/src"]
    S_resolveWebView2 --> T_copyWebView2["Copy WebView2Loader.dll -> artifactDevRoot/WebView2Loader.dll"]

    T_copyWebView2 --> U_buildBundles["npm run tauri build -- --target x86_64-pc-windows-gnu --bundles msi nsis"]

    U_buildBundles --> V_artifactReleaseRoot["artifactReleaseRoot = ArtifactsRoot/release; mkdir"]
    V_artifactReleaseRoot --> W_cleanOldExe["Remove existing artifactReleaseRoot/openless.exe if present"]
    W_cleanOldExe --> X_checkBundle{"bundle folder exists under releaseRoot?"}

    X_checkBundle -->|Yes| Y_copyBundle["Copy releaseRoot/bundle -> artifactReleaseRoot (recursive)"]
    X_checkBundle -->|No| Z_skipBundle["Skip bundle copy"]

    Y_copyBundle --> AA_cleanupMirror
    Z_skipBundle --> AA_cleanupMirror{"usedMirror and not KeepMirror?"}

    AA_cleanupMirror -->|Yes| AB_validateMirrorPath{"MirrorRoot under TEMP and leaf == 'openless-windows-gnu'?"}
    AB_validateMirrorPath -->|Yes| AC_removeMirror["Remove scratch build root MirrorRoot"]
    AB_validateMirrorPath -->|No| AD_warnMirror["Warn: refusing to remove unexpected mirror path"]

    AA_cleanupMirror -->|No| AE_noMirrorCleanup["Skip mirror cleanup"]

    AC_removeMirror --> AF_printSummary
    AD_warnMirror --> AF_printSummary
    AE_noMirrorCleanup --> AF_printSummary["Print artifact paths: ArtifactsRoot/dev/openless.exe and release bundles"]

    AF_printSummary --> AG_end["End windows-build-gnu.ps1"]
Loading

File-Level Changes

Change Details Files
Refactor Windows GNU build script to emit dev and release artifacts into a stable .artifacts directory and clean scratch mirrors safely.
  • Add ArtifactsRoot and KeepMirror parameters and track whether a mirror build root is used.
  • Exclude .artifacts from robocopy mirror and default ArtifactsRoot to .artifacts/windows-gnu under the repo.
  • Split Tauri build into a no-bundle dev binary build and a bundles-only build, copying openless.exe and WebView2Loader.dll into .artifacts/windows-gnu/dev.
  • Copy bundle outputs into .artifacts/windows-gnu/release and log their paths instead of target directory paths.
  • Introduce Resolve-WebView2Loader helper to locate the x64 WebView2Loader.dll and add guarded cleanup of the scratch mirror under %TEMP%.
openless-all/app/scripts/windows-build-gnu.ps1
Change Tauri app startup to avoid unconditional main window show, make it opt-in via env var, and harden tray icon creation when the default icon is missing.
  • Remove unconditional show_main_window(app.handle()) from the setup callback.
  • Wrap TrayIconBuilder usage in a check that default_window_icon() returns Some, logging a warning and skipping tray when missing.
  • Gate startup-time show_main_window behind OPENLESS_SHOW_MAIN_ON_START=1 so tests/dev tools can explicitly request it.
openless-all/app/src-tauri/src/lib.rs
Adjust React app startup so permissions gating does not block the initial Windows frame and show a lightweight startup shell during permission checks on other platforms.
  • Detect OS in App component and initialize gate as 'ready' on Windows Tauri instead of 'checking'.
  • Add an effect to show the current Tauri window on the next animation frame once the React app mounts, with error logging on failure.
  • Move accessibility/microphone permission checks into a separate effect that runs only for non-Windows Tauri, and render a StartupShell component while gate==='checking'.
  • Introduce StartupShell component with minimal centered branding and gradient background for the initial loading view.
openless-all/app/src/App.tsx
Add Windows runtime smoke and dev-launch helper scripts that operate on the new .artifacts-based dev executable.
  • Add windows-runtime-smoke.ps1 that locates the dev openless.exe under .artifacts, inspects credential configuration, launches the app, verifies responsiveness and hotkey listener log output, and then terminates all openless processes.
  • Add windows-open-dev.ps1 that reuses an existing OpenLess window if present, otherwise validates the dev exe and WebView2Loader.dll, launches with OPENLESS_SHOW_MAIN_ON_START=1, and brings the main window to the foreground using user32.dll.
  • Both scripts default ExePath to .artifacts/windows-gnu/dev/openless.exe and set minimal environment variables for Windows runtime.
openless-all/app/scripts/windows-runtime-smoke.ps1
openless-all/app/scripts/windows-open-dev.ps1

Possibly linked issues

  • #windows/devex: PR changes build artifacts to .artifacts, bundles WebView2, and adds windows-open-dev script to reliably show main window.
  • #[windows] 启动阶段先显示空边框再长时间白屏: PR推迟show窗口并新增StartupShell,避免权限探测期间的空边框与白屏,正对该Windows启动问题

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In windows-open-dev.ps1, clearing OPENLESS_SHOW_MAIN_ON_START with Remove-Item Env:OPENLESS_SHOW_MAIN_ON_START will also wipe any pre-existing value from the caller’s environment; consider capturing and restoring the previous value instead of unconditionally removing it.
  • The .artifacts\windows-gnu path and dev/release layout are now duplicated between windows-build-gnu.ps1, windows-open-dev.ps1 and windows-runtime-smoke.ps1; it may be worth centralizing this convention (e.g. via a small shared helper or constant) to avoid drift if the artifact layout changes again.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `windows-open-dev.ps1`, clearing `OPENLESS_SHOW_MAIN_ON_START` with `Remove-Item Env:OPENLESS_SHOW_MAIN_ON_START` will also wipe any pre-existing value from the caller’s environment; consider capturing and restoring the previous value instead of unconditionally removing it.
- The `.artifacts\windows-gnu` path and `dev`/`release` layout are now duplicated between `windows-build-gnu.ps1`, `windows-open-dev.ps1` and `windows-runtime-smoke.ps1`; it may be worth centralizing this convention (e.g. via a small shared helper or constant) to avoid drift if the artifact layout changes again.

## Individual Comments

### Comment 1
<location path="openless-all/app/scripts/windows-runtime-smoke.ps1" line_range="103-104" />
<code_context>
+  Copy-Item -LiteralPath (Resolve-WebView2Loader) -Destination (Join-Path $artifactDevRoot "WebView2Loader.dll") -Force
+
+  npm run tauri build -- --target x86_64-pc-windows-gnu --bundles msi nsis
 } finally {
   Pop-Location
 }
</code_context>
<issue_to_address>
**issue (bug_risk):** Stopping all `openless` processes on the machine may kill unrelated user sessions

The teardown logic appears to call `Get-Process openless | Stop-Process -Force`, which would kill every `openless.exe` on the machine, including existing user sessions. Consider tracking and stopping only the process you launch (e.g. via `$process`), and treating other running instances more gently (e.g. warn instead of force-killing) to avoid side effects.
</issue_to_address>

### Comment 2
<location path="openless-all/app/scripts/windows-runtime-smoke.ps1" line_range="47-52" />
<code_context>
+  }
+}
+
+function Wait-LogPattern($Path, $Pattern, $TimeoutSeconds) {
+  $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
+  while ((Get-Date) -lt $deadline) {
+    if ((Test-Path $Path) -and ((Get-Content -Raw $Path) -match $Pattern)) {
+      return $true
+    }
</code_context>
<issue_to_address>
**suggestion:** Log polling should tolerate transient read errors on the log file

Here `Get-Content -Raw $Path` can intermittently fail (e.g. sharing violations during log rotation or exclusive locks), which will terminate the whole smoke script instead of allowing another poll.

Consider treating these as transient by adding `-ErrorAction SilentlyContinue` or wrapping the read in try/catch and treating failures like a non-match so the loop continues until timeout.

```suggestion
function Wait-LogPattern($Path, $Pattern, $TimeoutSeconds) {
  $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
  while ((Get-Date) -lt $deadline) {
    if (Test-Path $Path) {
      try {
        $content = Get-Content -Raw -ErrorAction Stop $Path
        if ($content -match $Pattern) {
          return $true
        }
      } catch {
        # Treat transient read errors (e.g. sharing violations) as non-matches
        # and allow the loop to continue until the timeout is reached.
      }
    }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +103 to +104
} finally {
Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Stopping all openless processes on the machine may kill unrelated user sessions

The teardown logic appears to call Get-Process openless | Stop-Process -Force, which would kill every openless.exe on the machine, including existing user sessions. Consider tracking and stopping only the process you launch (e.g. via $process), and treating other running instances more gently (e.g. warn instead of force-killing) to avoid side effects.

Comment on lines +47 to +52
function Wait-LogPattern($Path, $Pattern, $TimeoutSeconds) {
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
if ((Test-Path $Path) -and ((Get-Content -Raw $Path) -match $Pattern)) {
return $true
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Log polling should tolerate transient read errors on the log file

Here Get-Content -Raw $Path can intermittently fail (e.g. sharing violations during log rotation or exclusive locks), which will terminate the whole smoke script instead of allowing another poll.

Consider treating these as transient by adding -ErrorAction SilentlyContinue or wrapping the read in try/catch and treating failures like a non-match so the loop continues until timeout.

Suggested change
function Wait-LogPattern($Path, $Pattern, $TimeoutSeconds) {
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
if ((Test-Path $Path) -and ((Get-Content -Raw $Path) -match $Pattern)) {
return $true
}
function Wait-LogPattern($Path, $Pattern, $TimeoutSeconds) {
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
if (Test-Path $Path) {
try {
$content = Get-Content -Raw -ErrorAction Stop $Path
if ($content -match $Pattern) {
return $true
}
} catch {
# Treat transient read errors (e.g. sharing violations) as non-matches
# and allow the loop to continue until the timeout is reached.
}
}

@appergb appergb merged commit ff694c0 into Open-Less:main Apr 30, 2026
2 checks passed
appergb pushed a commit that referenced this pull request Apr 30, 2026
包含本轮所有合并:
- Codex 终审两条 HIGH (cancel race) 修复 (PR #79)
- 6 个 Cooper-X-Oak/Codex bot PRs 自动合并 (#44 #49 #53 #68 #72 #73)
- 2 个有冲突 PR 本地 rebase 后合并 (#66 cancel + 空转写并存 / #67 Windows docs)
- README 破图修复 (PR #80)
- workflow-scope 受限的 #48 + #75 由用户在 GitHub UI 直接合并

3 处版本字段同步:package.json + tauri.conf.json + Cargo.toml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants