Public/Start-GreenroomSession.ps1

# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Tyler Vigario

<#
.SYNOPSIS
  Start an instance that is stopped. Leave one that is running alone.

.DESCRIPTION
  The counterpart to Stop-GreenroomSession, and the gap it left. Until this existed the
  only way to bring a stopped instance back was Restart-GreenroomSession, which is the
  wrong verb and the wrong behaviour: against an instance that turns out to be RUNNING it
  kills the watchdog, the session and the launcher first. Start never kills anything. An
  instance that is already up is returned as it is.

  It also takes the instance NAME, resolved against registered scheduled tasks, rather
  than a running session -- because the thing being started has no running session.
  Get-GreenroomInstance lists only running sessions, so after a stop it returns nothing,
  and piping it anywhere does nothing. MEASURED: the upgrade procedure documented in 0.4.0,
  `Get-GreenroomInstance | Restart-GreenroomSession` after stopping everything, brought
  nothing back up. Capture the names while they are running instead -- see the example.

  NO ELEVATION IS NEEDED TO START, even for an elevated instance, and this deliberately
  does not escalate. The task launches at its own run level; starting it is not acting on
  the elevated process. Measured: an unelevated shell started an elevated instance through
  its task. What an unelevated shell cannot do is CONFIRM it -- an elevated session's
  command line is unreadable from a lower integrity level -- so it says so, rather than
  reporting a check that never ran.

  A running instance that is unreadable from this shell looks stopped, so the task is
  started anyway. That is harmless: the second watchdog finds the instance's named mutex
  already held and exits at once.

.PARAMETER Name
  The instance to start. Wildcards match every registered instance, so
  `Start-GreenroomSession *` starts all of them that are down. If omitted and exactly
  one instance is registered, that one is used. Accepts pipeline input, including names
  captured from Get-GreenroomInstance while the instances were running.

.PARAMETER TimeoutSeconds
  How long to wait for each session to appear. Default 45.

.OUTPUTS
  Greenroom.Instance for each instance that is up afterwards -- started, or already
  running. Nothing for one that could not be confirmed.

.EXAMPLE
  Start-GreenroomSession laptop-admin

.EXAMPLE
  Start-GreenroomSession *
  Start every registered instance that is not running.

.EXAMPLE
  $running = (Get-GreenroomInstance).Instance
  $running | Stop-GreenroomSession
  winget upgrade --id Anthropic.ClaudeCode
  $running | Start-GreenroomSession
  Upgrading the CLI every session holds open. The names are captured FIRST: after the
  stop Get-GreenroomInstance lists nothing, and this also brings back exactly what was
  running rather than an instance that was deliberately down.
#>

function Start-GreenroomSession {
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType('Greenroom.Instance')]
    param(
        [Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('Instance')]
        [SupportsWildcards()]
        [string]$Name,

        [ValidateRange(5, 300)]
        [int]$TimeoutSeconds = 45
    )

    process {
        $names = @(Resolve-InstanceName -Name $Name -Cmdlet $PSCmdlet)

        foreach ($n in $names) {
            $now = @(Get-GreenroomInstance -Name $n -WarningAction SilentlyContinue -ErrorAction SilentlyContinue)
            if ($now.Count -gt 0 -and -not $now[0].Opaque) {
                Write-Verbose "'$n' is already running (claude pid $($now[0].ClaudePid)) -- nothing to start"
                $now[0]
                continue
            }

            if (-not $PSCmdlet.ShouldProcess($n, 'Start-GreenroomSession')) { continue }

            # Same warning Restart gives, for the same reason: the task names a VERSIONED
            # asset path, so starting it brings the instance up on whatever version it was
            # registered with, not the module that is loaded.
            $asset = Get-InstanceAssetVersion -Name $n
            if ($asset -and $asset -ne $script:GreenroomModuleVersion) {
                Write-Warning ("'$n' runs $asset assets while module $script:GreenroomModuleVersion is loaded. " +
                               "Starting runs the task, so it will come up on $asset. To move it: " +
                               "Update-GreenroomInstance -Name $n")
            }

            Start-ScheduledTask -TaskName "greenroom-$n"

            # Confirm by observation: the task succeeding only means the watchdog was
            # launched, not that a session came up behind it. Counted polls rather than a
            # wall-clock deadline, so a sleep that does not sleep cannot become a busy-wait.
            $up = $null
            for ($i = 0; $i -lt $TimeoutSeconds * 2; $i++) {
                Start-Sleep -Milliseconds 500
                $got = @(Get-GreenroomInstance -Name $n -WarningAction SilentlyContinue -ErrorAction SilentlyContinue)
                if ($got.Count -gt 0 -and -not $got[0].Opaque) { $up = $got[0]; break }
            }
            if ($up) { $up; continue }

            if ((Test-InstanceElevated -Name $n) -and -not (Test-SelfElevated)) {
                Write-Warning ("started '$n', but cannot confirm it from an unelevated shell: an elevated " +
                               'session is unreadable here. Re-check with Get-GreenroomInstance from an elevated shell.')
                continue
            }

            Write-Error -Category OperationTimeout -Message (
                "'$n' did not come up within $TimeoutSeconds s. If its watchdog is still alive -- in crash-loop " +
                "backoff, say -- the new one exits on the instance's mutex and nothing starts until the backoff " +
                "ends; Restart-GreenroomSession $n stops it first. Check: Get-Content " +
                "`"$(Join-Path (Get-GreenroomStateRoot) "$n\watchdog.log")`" -Tail 20")
        }
    }
}