Public/Monitor.ps1
|
# --------------------------------------------------------------------------- # Monitor - background scanning for dead sessions # --------------------------------------------------------------------------- function Start-TTMonitor { <# .SYNOPSIS Start the background session monitor. .DESCRIPTION Starts a polling loop that periodically checks tracked sessions, archives dead ones, updates working directories via PEB, discovers new terminals, and triggers sync if configured. .PARAMETER AsJob Run the monitor as a background PowerShell job (recommended). .EXAMPLE Start-TTMonitor -AsJob Starts the monitor as a background job. .EXAMPLE Start-TTMonitor Runs the monitor in the foreground (Ctrl+C to stop). #> [CmdletBinding(SupportsShouldProcess)] param( [switch]$AsJob ) Initialize-TTDataStore if ($AsJob) { # Check for existing running monitor $existing = Get-Job -Name 'TerminalTracker-Monitor' -ErrorAction SilentlyContinue | Where-Object { $_.State -eq 'Running' } if ($existing) { Write-Verbose "Monitor already running (Job ID: $($existing.Id))." return $existing } # Clean up dead monitor jobs Get-Job -Name 'TerminalTracker-Monitor' -ErrorAction SilentlyContinue | Where-Object { $_.State -ne 'Running' } | Remove-Job -Force -ErrorAction SilentlyContinue # Capture module path as a string BEFORE creating the job # $PSScriptRoot is not reliable inside a job scriptblock $modulePath = (Get-Module TerminalTracker).Path if (-not $modulePath) { $modulePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'TerminalTracker.psm1' } $jobScript = { param($ModulePath) Import-Module $ModulePath -Force -DisableNameChecking while ($true) { try { Invoke-TTMonitorCycle } catch { Write-Verbose "Monitor cycle error: $_" } $cfg = Get-TTConfig Start-Sleep -Seconds $cfg.MonitorIntervalSeconds } } $job = Start-Job -ScriptBlock $jobScript -ArgumentList $modulePath -Name 'TerminalTracker-Monitor' Write-Verbose "Monitor started as background job (ID: $($job.Id))." return $job } else { Write-Verbose "Running monitor in foreground (Ctrl+C to stop)..." while ($true) { Invoke-TTMonitorCycle $cfg = Get-TTConfig Start-Sleep -Seconds $cfg.MonitorIntervalSeconds } } } function Stop-TTMonitor { <# .SYNOPSIS Stop the background session monitor. .DESCRIPTION Stops the background TerminalTracker-Monitor job if one is running. The job is stopped and removed. .EXAMPLE Stop-TTMonitor Stops the background monitor job. #> [CmdletBinding(SupportsShouldProcess)] param() $jobs = Get-Job -Name 'TerminalTracker-Monitor' -ErrorAction SilentlyContinue if ($jobs) { $jobs | Stop-Job -PassThru | Remove-Job -Force Write-Verbose "Monitor stopped." } else { Write-Verbose "No monitor job found." } } function Invoke-TTMonitorCycle { <# .SYNOPSIS Run one monitor cycle: clean dead sessions, discover new terminals, update CWDs. .DESCRIPTION Executes a single iteration of the monitor loop. Archives dead sessions, updates live session working directories via PEB, discovers new interactive terminals, and exports state to sync path if configured. .EXAMPLE Invoke-TTMonitorCycle Runs one monitor cycle manually. #> [CmdletBinding()] param() Initialize-TTDataStore $sessions = @(Read-JsonFile $script:SessionsFile) $changed = $false # Phase 1: Check existing sessions - archive dead ones, update live ones $alive = @() foreach ($s in $sessions) { $running = try { $null -ne (Get-Process -Id $s.Pid -ErrorAction Stop); $true } catch { $false } if ($running) { # Update CWD from PEB (more reliable than prompt hook for auto-discovered sessions) $cwd = Get-ProcessCwd -ProcessId $s.Pid if ($cwd -and $cwd -ne $s.WorkingDirectory -and $cwd -ne '(unknown)') { # Record directory change in history if ($s.WorkingDirectory -and $s.WorkingDirectory -ne '(unknown)') { if (-not $s.DirectoryHistory) { $s | Add-Member -NotePropertyName DirectoryHistory -NotePropertyValue @() -Force } $histEntry = [PSCustomObject]@{ Directory = $s.WorkingDirectory; Timestamp = $s.LastUpdated } $history = @($s.DirectoryHistory) + @($histEntry) if ($history.Count -gt 50) { $history = $history[-50..-1] } $s.DirectoryHistory = $history } $s.WorkingDirectory = $cwd $changed = $true } $s.LastSeen = [datetime]::UtcNow.ToString('o') $alive += $s } else { Add-TTArchiveEntry $s 'closed' $changed = $true } } if ($changed) { Write-JsonFile $script:SessionsFile $alive } # Phase 2: Discover new interactive terminals $trackedPids = @($alive | ForEach-Object { $_.Pid }) $discovered = Get-InteractiveTerminal # Backfill WTHost on existing sessions that lack it (one-time migration) $discoveredByPid = @{} foreach ($d in $discovered) { $discoveredByPid[$d.Pid] = $d } foreach ($s in $alive) { if ($s.AutoDiscovered -and -not $s.WTHost -and $discoveredByPid.ContainsKey($s.Pid)) { $wtHost = $discoveredByPid[$s.Pid].WTHost if ($wtHost) { $s | Add-Member -NotePropertyName WTHost -NotePropertyValue $wtHost -Force $changed = $true } } } if ($changed) { Write-JsonFile $script:SessionsFile $alive } $newSessions = @() foreach ($term in $discovered) { if ($term.Pid -in $trackedPids) { continue } if ($term.Pid -eq $PID) { continue } $newSession = [PSCustomObject]@{ Id = [guid]::NewGuid().ToString('N').Substring(0, 8) Pid = $term.Pid ProcessName = $term.ProcessName Shell = $term.Shell WindowTitle = $term.WindowTitle WorkingDirectory = $term.WorkingDirectory StartTime = $term.StartTime LastUpdated = [datetime]::UtcNow.ToString('o') LastSeen = [datetime]::UtcNow.ToString('o') Notes = '' Hidden = $false Tags = @() DirectoryHistory = @() AutoDiscovered = $true WTHost = $term.WTHost } $newSessions += $newSession } if ($newSessions.Count -gt 0) { $sessions = @(Read-JsonFile $script:SessionsFile) $sessions = @($sessions) + @($newSessions) Write-JsonFile $script:SessionsFile $sessions } # Sync if configured $cfg = Get-TTConfig if ($cfg.SyncPath) { Export-TTState -Path $cfg.SyncPath -Quiet } } |