Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions psmux-continuum/plugin.conf
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
#
# NOTE: The auto_save.ps1 script runs a persistent loop (sleeping between
# saves). run-shell executes it asynchronously so it won't block psmux.
# If you attach multiple clients, multiple loops may start — this is
# harmless since saves are idempotent.
# It enforces a per-user singleton via PID file at
# %LOCALAPPDATA%\psmux-continuum\auto_save.pid, so re-firing this hook on
# every client attach will NOT spawn additional loops.

# Start auto-save background loop on client attach (every 15 minutes)
set-hook -g client-attached 'run-shell "pwsh -NoProfile -File \"~/.psmux/plugins/psmux-continuum/scripts/auto_save.ps1\" -IntervalMinutes 15"'
Expand Down
120 changes: 101 additions & 19 deletions psmux-continuum/psmux-continuum.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@ if (-not (Test-Path $SCRIPTS_DIR)) {
}

# --- Create the auto-save background script ---
# NOTE: The auto_save loop is a per-user singleton enforced by a PID file
# at $env:LOCALAPPDATA\psmux-continuum\auto_save.pid. This means the
# client-attached hook in plugin.conf can re-fire on every re-attach
# without accumulating long-running pwsh processes — duplicate launches
# detect the live owner and exit immediately.
$autoSaveScript = @'
#!/usr/bin/env pwsh
# DO NOT EDIT — regenerated from psmux-continuum.ps1 on plugin load.
# psmux-continuum: Background auto-save loop
param(
[int]$IntervalMinutes = 15
Expand All @@ -50,6 +56,45 @@ function Get-PsmuxBin {
return 'psmux'
}

# --- Singleton guard: one auto-save loop per user ---
$pidDir = Join-Path $env:LOCALAPPDATA 'psmux-continuum'
$pidFile = Join-Path $pidDir 'auto_save.pid'
$logFile = Join-Path $pidDir 'auto_save.log'
New-Item -ItemType Directory -Path $pidDir -Force -ErrorAction SilentlyContinue | Out-Null

# Rotate the log if it grew past 256 KB. Add-Content reopens per write, so
# concurrent invocations don't corrupt each other's writes.
if ((Test-Path $logFile) -and ((Get-Item $logFile).Length -gt 262144)) {
Move-Item $logFile "$logFile.old" -Force -ErrorAction SilentlyContinue
}

function Log {
param([string]$Message, [string]$Level = 'INF')
try {
"[{0}] [{1}] [PID {2}] {3}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $PID, $Message |
Add-Content -Path $logFile -Encoding UTF8 -ErrorAction Stop
}
catch {}
}

# If a live owner already holds the PID file, defer.
$existingPid = $null
try {
$existingPid = (Get-Content $pidFile -Raw -ErrorAction Stop).Trim()
}
catch {}
if ($existingPid -match '^\d+$') {
$existing = Get-Process -Id ([int]$existingPid) -ErrorAction SilentlyContinue
if ($existing -and ($existing.ProcessName -in @('pwsh','powershell'))) {
Log "auto-save already running (PID $existingPid), exiting."
exit 0
}
}

# Claim the slot
"$PID" | Set-Content -Path $pidFile -Encoding UTF8 -Force
Comment on lines +80 to +95
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@CosminRadu Can you please review this comment from CoPilot?

Log "claimed singleton slot; interval=${IntervalMinutes}m"

$PSMUX = Get-PsmuxBin

# Find the resurrect save script
Expand All @@ -59,25 +104,59 @@ if (-not (Test-Path $saveScript)) {
}

if (-not (Test-Path $saveScript)) {
Write-Host "psmux-continuum: psmux-resurrect not found. Install it first." -ForegroundColor Red
Log "psmux-resurrect not found. Install it first." 'ERR'
Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
exit 1
}

$IntervalSeconds = $IntervalMinutes * 60

while ($true) {
Start-Sleep -Seconds $IntervalSeconds

# Check if psmux server is still running
$sessions = & $PSMUX ls 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
Write-Host "psmux-continuum: Server not running, stopping auto-save." -ForegroundColor Yellow
break
$iter = 0

try {
while ($true) {
Start-Sleep -Seconds $IntervalSeconds

# Graceful supersede: if the PID file no longer points to us, exit.
$owner = $null
try {
$owner = (Get-Content $pidFile -Raw -ErrorAction Stop).Trim()
}
catch {}
if ($owner -ne "$PID") {
Log "superseded, exiting."
break
}

# Check if psmux server is still running
$sessions = & $PSMUX ls 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
Log "psmux server not running, stopping auto-save." 'WRN'
break
}

# Run the save; capture all streams (output, errors, warnings) into the log.
& pwsh -NoProfile -File $saveScript *>> $logFile
Log "auto-saved."

# Bound memory growth in this long-running loop
$iter++
if (($iter % 4) -eq 0) {
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
[GC]::Collect()
}
}

# Run the save
& pwsh -NoProfile -File $saveScript
Write-Host "psmux-continuum: Auto-saved at $(Get-Date -Format 'HH:mm:ss')" -ForegroundColor DarkGray
}
finally {
Log "exiting; releasing singleton slot."
# Release singleton slot only if we still own it
try {
$owner = (Get-Content $pidFile -Raw -ErrorAction Stop).Trim()
if ($owner -eq "$PID") {
Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
}
}
catch {}
}
'@

Expand All @@ -86,6 +165,7 @@ Set-Content -Path (Join-Path $SCRIPTS_DIR 'auto_save.ps1') -Value $autoSaveScrip
# --- Create the auto-restore script ---
$autoRestoreScript = @'
#!/usr/bin/env pwsh
# DO NOT EDIT — regenerated from psmux-continuum.ps1 on plugin load.
# psmux-continuum: Auto-restore on server start
$ErrorActionPreference = 'Continue'

Expand All @@ -107,6 +187,7 @@ Set-Content -Path (Join-Path $SCRIPTS_DIR 'auto_restore.ps1') -Value $autoRestor
# --- Create boot script ---
$bootScript = @'
#!/usr/bin/env pwsh
# DO NOT EDIT — regenerated from psmux-continuum.ps1 on plugin load.
# psmux-continuum: Register/unregister psmux auto-start on Windows login
param(
[switch]$Enable,
Expand Down Expand Up @@ -143,7 +224,7 @@ if ($Enable) {

Set-Content -Path (Join-Path $SCRIPTS_DIR 'boot.ps1') -Value $bootScript -Force

# --- Start auto-save background job ---
# --- Start auto-save background loop ---
$interval = 15 # Default 15 minutes

# Try to read interval from psmux options
Expand All @@ -154,10 +235,11 @@ if ($intervalOpt -match '^\d+$') {

if ($interval -gt 0) {
$autoSavePath = Join-Path $SCRIPTS_DIR 'auto_save.ps1'
Start-Job -ScriptBlock {
param($script, $interval)
& pwsh -NoProfile -File $script -IntervalMinutes $interval
} -ArgumentList $autoSavePath, $interval | Out-Null
# Detached, hidden pwsh — single process, not a Start-Job wrapper.
# The singleton guard inside auto_save.ps1 makes repeated launches safe.
Start-Process -FilePath 'pwsh' `
-ArgumentList @('-NoProfile','-File',$autoSavePath,'-IntervalMinutes',"$interval") `
-WindowStyle Hidden | Out-Null
}

# --- Auto-restore on first load ---
Expand Down
1 change: 1 addition & 0 deletions psmux-continuum/scripts/auto_restore.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env pwsh
# DO NOT EDIT — regenerated from psmux-continuum.ps1 on plugin load.
# psmux-continuum: Auto-restore on server start
$ErrorActionPreference = 'Continue'

Expand Down
98 changes: 86 additions & 12 deletions psmux-continuum/scripts/auto_save.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env pwsh
# DO NOT EDIT — regenerated from psmux-continuum.ps1 on plugin load.
# psmux-continuum: Background auto-save loop
param(
[int]$IntervalMinutes = 15
Expand All @@ -14,6 +15,45 @@ function Get-PsmuxBin {
return 'psmux'
}

# --- Singleton guard: one auto-save loop per user ---
$pidDir = Join-Path $env:LOCALAPPDATA 'psmux-continuum'
$pidFile = Join-Path $pidDir 'auto_save.pid'
$logFile = Join-Path $pidDir 'auto_save.log'
New-Item -ItemType Directory -Path $pidDir -Force -ErrorAction SilentlyContinue | Out-Null

# Rotate the log if it grew past 256 KB. Add-Content reopens per write, so
# concurrent invocations don't corrupt each other's writes.
if ((Test-Path $logFile) -and ((Get-Item $logFile).Length -gt 262144)) {
Move-Item $logFile "$logFile.old" -Force -ErrorAction SilentlyContinue
}

function Log {
param([string]$Message, [string]$Level = 'INF')
try {
"[{0}] [{1}] [PID {2}] {3}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $PID, $Message |
Add-Content -Path $logFile -Encoding UTF8 -ErrorAction Stop
}
catch {}
}

# If a live owner already holds the PID file, defer.
$existingPid = $null
try {
$existingPid = (Get-Content $pidFile -Raw -ErrorAction Stop).Trim()
}
catch {}
if ($existingPid -match '^\d+$') {
$existing = Get-Process -Id ([int]$existingPid) -ErrorAction SilentlyContinue
if ($existing -and ($existing.ProcessName -in @('pwsh','powershell'))) {
Log "auto-save already running (PID $existingPid), exiting."
exit 0
}
}

# Claim the slot
"$PID" | Set-Content -Path $pidFile -Encoding UTF8 -Force
Log "claimed singleton slot; interval=${IntervalMinutes}m"

$PSMUX = Get-PsmuxBin

# Find the resurrect save script
Expand All @@ -23,23 +63,57 @@ if (-not (Test-Path $saveScript)) {
}

if (-not (Test-Path $saveScript)) {
Write-Host "psmux-continuum: psmux-resurrect not found. Install it first." -ForegroundColor Red
Log "psmux-resurrect not found. Install it first." 'ERR'
Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
exit 1
}

$IntervalSeconds = $IntervalMinutes * 60
$iter = 0

while ($true) {
Start-Sleep -Seconds $IntervalSeconds
try {
while ($true) {
Start-Sleep -Seconds $IntervalSeconds

# Check if psmux server is still running
$sessions = & $PSMUX ls 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
Write-Host "psmux-continuum: Server not running, stopping auto-save." -ForegroundColor Yellow
break
}
# Graceful supersede: if the PID file no longer points to us, exit.
$owner = $null
try {
$owner = (Get-Content $pidFile -Raw -ErrorAction Stop).Trim()
}
catch {}
if ($owner -ne "$PID") {
Log "superseded, exiting."
break
}

# Check if psmux server is still running
$sessions = & $PSMUX ls 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
Log "psmux server not running, stopping auto-save." 'WRN'
break
}

# Run the save
& pwsh -NoProfile -File $saveScript
Write-Host "psmux-continuum: Auto-saved at $(Get-Date -Format 'HH:mm:ss')" -ForegroundColor DarkGray
# Run the save; capture all streams (output, errors, warnings) into the log.
& pwsh -NoProfile -File $saveScript *>> $logFile
Log "auto-saved."

# Bound memory growth in this long-running loop
$iter++
if (($iter % 4) -eq 0) {
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
[GC]::Collect()
}
}
}
finally {
Log "exiting; releasing singleton slot."
# Release singleton slot only if we still own it
try {
$owner = (Get-Content $pidFile -Raw -ErrorAction Stop).Trim()
if ($owner -eq "$PID") {
Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
}
}
catch {}
}
1 change: 1 addition & 0 deletions psmux-continuum/scripts/boot.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env pwsh
# DO NOT EDIT — regenerated from psmux-continuum.ps1 on plugin load.
# psmux-continuum: Register/unregister psmux auto-start on Windows login
param(
[switch]$Enable,
Expand Down