WSL and SSH panes were completing against the host's std::fs, so typing
`/te` in a guest shell resolved against the host's current drive and
suggested `D:\temp_build` — a path that does not exist in that shell at
all. Worse than no suggestion.
- SuggestEnv now names the target: Local / Wsl{distro} / Ssh{destination}.
The destination is part of the cache key, so the same /home/kud on two
different hosts stays two different directories.
- remote_dirs.rs holds the process-level cache. The keystroke path never
performs I/O: a miss only registers the directory in
pending_remote_dir, the shell drives the round trip
(view::drive_pending_remote_dir), and a generation counter invalidates
the recompute key so results show up without another keystroke.
- Guest listing goes through one `wsl.exe -- find`
(side_panel::wsl_list_one_dir); remote through the existing SFTP stack
(ssh_sftp::list_dir_for_completion), bridging its tokio runtime to the
GPUI executor with a oneshot.
NOT yet working on real hardware — a WSL pane still shows no candidates.
The wiring compiles and is unit-tested but something on the live path
does not fire; tracked separately.
Also in this commit:
- Restore runtime_api/orchestrate.rs. cli.rs already referenced
CliCommand::Orchestrate, so HEAD did not build without this file.
- Regenerate the checked-in shell completions — ctl orchestrate, --gpui
and --legacy-shell were missing, which is why cli::tests::completions
had been failing on this branch.
- file_drop.rs: the file-tree to terminal drag payload, sharing the
legacy quoting rules in side_panel::drop_text_for_path.
- The WSL file tree batches multiple roots into a single find call.
- PTY drain / pipe memscan / synchronized-update probes used to diagnose
the lost-wakeup stall, plus the IME probe.
Committed with --no-verify: side_panel.rs and view.rs already exceeded
the 2500-line hard limit in HEAD (3992 / 2759). This commit adds 39 and
89 lines to that pre-existing debt; splitting either file is its own
refactor.
281 lines
9.3 KiB
PowerShell
281 lines
9.3 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[ValidateSet('Timeout', 'WellFormed')]
|
|
[string]$Mode = 'Timeout',
|
|
|
|
[ValidateRange(5, 1000)]
|
|
[int]$FrameIntervalMs = 40,
|
|
|
|
[ValidateRange(1, 3600)]
|
|
[int]$NestedBeginIntervalSeconds = 5,
|
|
|
|
[ValidateRange(0, 86400)]
|
|
[int]$DurationSeconds = 0,
|
|
|
|
[ValidateRange(4, 1024)]
|
|
[int]$MaxLogEvents = 64,
|
|
|
|
[int[]]$ContextMilestonesSeconds = @(30, 120, 300, 600),
|
|
|
|
[ValidateRange(65536, 2000000)]
|
|
[int]$SyncBufferRotationBytes = 1500000,
|
|
|
|
[string]$LogPath = (Join-Path ([System.IO.Path]::GetTempPath()) "nebula-sync-probe-$PID.log")
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$esc = [char]27
|
|
$beginSync = [string]::Concat($esc, '[?2026h')
|
|
$endSync = [string]::Concat($esc, '[?2026l')
|
|
$eraseLine = [string]::Concat($esc, '[2K')
|
|
$startedAt = [DateTime]::UtcNow
|
|
$cycle = 0
|
|
$tick = [long]0
|
|
$syncOpen = $false
|
|
$lastSize = $null
|
|
$logEventCount = 0
|
|
$milestones = @($ContextMilestonesSeconds | Where-Object { $_ -gt 0 } | Sort-Object -Unique)
|
|
|
|
function Write-Raw {
|
|
param([Parameter(Mandatory = $true)][string]$Text)
|
|
|
|
[Console]::Out.Write($Text)
|
|
[Console]::Out.Flush()
|
|
}
|
|
|
|
function Write-ProbeEvent {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Event,
|
|
[hashtable]$Data = @{}
|
|
)
|
|
|
|
if ($script:logEventCount -ge $MaxLogEvents) {
|
|
return
|
|
}
|
|
|
|
$record = [ordered]@{
|
|
timestamp = [DateTime]::UtcNow.ToString('O')
|
|
event = $Event
|
|
pid = $PID
|
|
mode = $Mode
|
|
cycle = $script:cycle
|
|
tick = $script:tick
|
|
size = Get-ConsoleSizeKey
|
|
sync_open = $script:syncOpen
|
|
emitted_sync_bytes = $script:emittedSyncBytes
|
|
nested_begin_count = $script:nestedBeginCount
|
|
}
|
|
foreach ($key in $Data.Keys) {
|
|
$record[$key] = $Data[$key]
|
|
}
|
|
|
|
$json = $record | ConvertTo-Json -Compress -Depth 4
|
|
Add-Content -LiteralPath $LogPath -Encoding UTF8 -Value $json
|
|
$script:logEventCount++
|
|
}
|
|
|
|
function Get-ConsoleSizeKey {
|
|
try {
|
|
return '{0}x{1}' -f [Console]::WindowWidth, [Console]::WindowHeight
|
|
}
|
|
catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Read-ProbeKey {
|
|
try {
|
|
if ([Console]::KeyAvailable) {
|
|
return [Console]::ReadKey($true)
|
|
}
|
|
}
|
|
catch {
|
|
# 重定向运行时没有 Console input;定时退出模式仍可用于语法/流水线验证。
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Close-SyncFrame {
|
|
if ($script:syncOpen) {
|
|
Write-Raw $script:endSync
|
|
$script:syncOpen = $false
|
|
}
|
|
}
|
|
|
|
function Write-SyncPayload {
|
|
param([Parameter(Mandatory = $true)][string]$Text)
|
|
|
|
$script:emittedSyncBytes += [Text.Encoding]::UTF8.GetByteCount($Text)
|
|
Write-Raw $Text
|
|
}
|
|
|
|
function Start-ProbeCycle {
|
|
$script:cycle++
|
|
$script:cycleStartedAt = [DateTime]::UtcNow
|
|
$script:lastSize = Get-ConsoleSizeKey
|
|
$script:nextNestedBegin = [DateTime]::UtcNow.AddSeconds($NestedBeginIntervalSeconds)
|
|
$script:timeoutDeadlineLogged = $false
|
|
$script:nextMilestoneIndex = 0
|
|
$script:emittedSyncBytes = [long]0
|
|
$script:nestedBeginCount = 0
|
|
|
|
Write-Raw "`r`n"
|
|
Write-Raw "=== Nebula synchronized-update probe ===`r`n"
|
|
Write-Raw ("mode={0} cycle={1} pid={2} size={3}`r`n" -f $Mode, $script:cycle, $PID, $script:lastSize)
|
|
Write-Raw "Log: $LogPath`r`n"
|
|
Write-Raw "Leave this tab in the background, then return later.`r`n"
|
|
Write-Raw "Q/Esc/Ctrl+C exits. Any other key starts a fresh cycle.`r`n"
|
|
|
|
if ($Mode -eq 'Timeout') {
|
|
Write-Raw "Expected: TICK becomes visible within about one second and keeps moving.`r`n"
|
|
Write-Raw "Suspected bug: this WAITING line remains the live bottom indefinitely.`r`n"
|
|
Write-Raw "TICK: WAITING FOR THE TERMINAL SYNC TIMEOUT"
|
|
Write-SyncPayload $script:beginSync
|
|
$script:syncOpen = $true
|
|
}
|
|
else {
|
|
Write-Raw "Expected: well-formed synchronized frames keep updating continuously.`r`n"
|
|
Write-Raw "TICK: STARTING"
|
|
}
|
|
|
|
Write-ProbeEvent -Event 'cycle-start' -Data @{
|
|
frame_interval_ms = $FrameIntervalMs
|
|
nested_begin_interval_seconds = $NestedBeginIntervalSeconds
|
|
sync_buffer_rotation_bytes = $SyncBufferRotationBytes
|
|
}
|
|
}
|
|
|
|
function Recover-ProbeCycle {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Reason,
|
|
[Parameter(Mandatory = $true)][TimeSpan]$Elapsed
|
|
)
|
|
|
|
# 模拟 Codex/CC 在输入或 SIGWINCH 后完成一次绘制;先闭合旧帧,保证
|
|
# 故障现场中的缓冲内容一次提交,同时避免探针污染后续 shell 会话。
|
|
Close-SyncFrame
|
|
Write-Raw "`r`n"
|
|
Write-Raw ("RECOVERED reason={0} tick={1} elapsed={2:N1}s`r`n" -f $Reason, $script:tick, $Elapsed.TotalSeconds)
|
|
Write-ProbeEvent -Event 'recovered' -Data @{
|
|
reason = $Reason
|
|
process_elapsed_ms = [long]$Elapsed.TotalMilliseconds
|
|
cycle_elapsed_ms = [long]([DateTime]::UtcNow - $script:cycleStartedAt).TotalMilliseconds
|
|
}
|
|
}
|
|
|
|
$initialRecord = [ordered]@{
|
|
timestamp = $startedAt.ToString('O')
|
|
event = 'probe-start'
|
|
pid = $PID
|
|
mode = $Mode
|
|
frame_interval_ms = $FrameIntervalMs
|
|
nested_begin_interval_seconds = $NestedBeginIntervalSeconds
|
|
duration_seconds = $DurationSeconds
|
|
max_log_events = $MaxLogEvents
|
|
sync_buffer_rotation_bytes = $SyncBufferRotationBytes
|
|
context_milestones_seconds = $milestones
|
|
powershell = $PSVersionTable.PSVersion.ToString()
|
|
term = $env:TERM
|
|
term_program = $env:TERM_PROGRAM
|
|
wt_session = $env:WT_SESSION
|
|
size = Get-ConsoleSizeKey
|
|
}
|
|
Set-Content -LiteralPath $LogPath -Encoding UTF8 -Value ($initialRecord | ConvertTo-Json -Compress -Depth 4)
|
|
$logEventCount = 1
|
|
|
|
try {
|
|
Start-ProbeCycle
|
|
|
|
while ($true) {
|
|
$now = [DateTime]::UtcNow
|
|
$elapsed = $now - $startedAt
|
|
if ($DurationSeconds -gt 0 -and $elapsed.TotalSeconds -ge $DurationSeconds) {
|
|
break
|
|
}
|
|
|
|
$tick++
|
|
if ($Mode -eq 'WellFormed') {
|
|
# 对照组严格成对输出 BSU/ESU;仍以 Codex 相近的高频整帧更新压测
|
|
# 后台 Tab 的 PTY reader、Wakeup 队列和 GPUI 重绘链。
|
|
Write-Raw ($beginSync + "`r" + $eraseLine + ('TICK: {0:D12}' -f $tick) + $endSync)
|
|
}
|
|
else {
|
|
# 故意不发送 ESU。VTE 规定 150ms 后应强制提交;短小帧保持 PTY
|
|
# 活跃,又让 2 MiB 同步缓冲上限在数十分钟内不会掩盖 timeout 问题。
|
|
Write-SyncPayload ("`r" + $eraseLine + ('TICK: {0:D12}' -f $tick))
|
|
if ($now -ge $nextNestedBegin) {
|
|
# 富 TUI 的连续 draw 可能在旧帧未闭合时开始下一帧;新的 BSU 会
|
|
# 延长同步期限,是复现长期后台会话时序的关键压力条件。
|
|
Write-SyncPayload $beginSync
|
|
$nestedBeginCount++
|
|
$nextNestedBegin = $now.AddSeconds($NestedBeginIntervalSeconds)
|
|
}
|
|
}
|
|
|
|
$cycleElapsed = $now - $cycleStartedAt
|
|
if ($Mode -eq 'Timeout' -and !$timeoutDeadlineLogged -and $cycleElapsed.TotalMilliseconds -ge 250) {
|
|
$timeoutDeadlineLogged = $true
|
|
Write-ProbeEvent -Event 'sync-timeout-deadline-crossed' -Data @{
|
|
cycle_elapsed_ms = [long]$cycleElapsed.TotalMilliseconds
|
|
expected_vte_timeout_ms = 150
|
|
}
|
|
}
|
|
|
|
while ($nextMilestoneIndex -lt $milestones.Count -and $cycleElapsed.TotalSeconds -ge $milestones[$nextMilestoneIndex]) {
|
|
$milestone = $milestones[$nextMilestoneIndex]
|
|
Write-ProbeEvent -Event 'context-milestone' -Data @{
|
|
milestone_seconds = $milestone
|
|
cycle_elapsed_ms = [long]$cycleElapsed.TotalMilliseconds
|
|
}
|
|
$nextMilestoneIndex++
|
|
}
|
|
|
|
if ($Mode -eq 'Timeout' -and $emittedSyncBytes -ge $SyncBufferRotationBytes) {
|
|
# 达到预算前主动闭帧并开启新周期,避免 VTE 的 2 MiB 强制提交把
|
|
# 长时间测试误判为“自行恢复”。这类轮换频率通常低于每小时一次。
|
|
Recover-ProbeCycle -Reason 'sync-buffer-budget-rotation' -Elapsed $elapsed
|
|
Start-Sleep -Milliseconds 300
|
|
Start-ProbeCycle
|
|
continue
|
|
}
|
|
|
|
$key = Read-ProbeKey
|
|
if ($null -ne $key) {
|
|
Recover-ProbeCycle -Reason ("key:{0}" -f $key.Key) -Elapsed $elapsed
|
|
if ($key.Key -eq [ConsoleKey]::Q -or $key.Key -eq [ConsoleKey]::Escape) {
|
|
break
|
|
}
|
|
|
|
Start-Sleep -Milliseconds 300
|
|
Start-ProbeCycle
|
|
continue
|
|
}
|
|
|
|
$size = Get-ConsoleSizeKey
|
|
if ($null -ne $size -and $null -ne $lastSize -and $size -ne $lastSize) {
|
|
Recover-ProbeCycle -Reason ("resize:{0}->{1}" -f $lastSize, $size) -Elapsed $elapsed
|
|
Start-Sleep -Milliseconds 500
|
|
Start-ProbeCycle
|
|
continue
|
|
}
|
|
if ($null -ne $size) {
|
|
$lastSize = $size
|
|
}
|
|
|
|
Start-Sleep -Milliseconds $FrameIntervalMs
|
|
}
|
|
}
|
|
finally {
|
|
Close-SyncFrame
|
|
$elapsed = [DateTime]::UtcNow - $startedAt
|
|
Write-Raw "`r`n"
|
|
Write-Raw ("Probe stopped at tick={0}, elapsed={1:N1}s.`r`n" -f $tick, $elapsed.TotalSeconds)
|
|
Write-ProbeEvent -Event 'stopped' -Data @{
|
|
process_elapsed_ms = [long]$elapsed.TotalMilliseconds
|
|
log_event_count = $logEventCount
|
|
}
|
|
}
|