/** * The scheduled task that starts an instance inside the signed-in Windows * desktop. Programs started straight from an SSH session run in a session * with no desktop, where OBS, osu! and window capture cannot work; a task * with LogonType Interactive runs in the user's own desktop session instead, * and keeps running when the SSH connection closes. */ export const taskName = (instance: string) => `Vash Broadcast (${instance})`; /** Runs a PowerShell script with values passed as environment variables (no quoting problems). */ export async function powershell(script: string, env: Record = {}) { if (process.platform !== 'win32') throw new Error('Scheduled tasks exist only on Windows'); // -EncodedCommand (UTF-16LE, base64) sidesteps Windows command-line quoting entirely. const encoded = Buffer.from( `$ProgressPreference = 'SilentlyContinue'\n${script}`, 'utf16le' ).toString('base64'); const child = Bun.spawn( [ 'powershell.exe', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded ], { stdout: 'pipe', stderr: 'pipe', env: { ...process.env, ...env }, windowsHide: true } ); const [stdout, stderr, code] = await Promise.all([ new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited ]); if (code !== 0) throw new Error( stderr.trim().split(/\r?\n/).slice(0, 3).join(' ') || `PowerShell exited with ${code}` ); return stdout.trim(); } export async function registerTask(options: { instance: string; exe: string; cwd: string; autostart: boolean; }) { await powershell( `$ErrorActionPreference = 'Stop' $user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $action = New-ScheduledTaskAction -Execute $env:VASH_EXE -Argument $env:VASH_ARGS -WorkingDirectory $env:VASH_CWD $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited # Priority 4 is normal; the default (7) would slow OBS and osu! down. $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew -StartWhenAvailable -Priority 4 $task = @{ Action = $action; Principal = $principal; Settings = $settings; Description = 'Vash broadcast worker. Manage it with vash-broadcast.exe.' } if ($env:VASH_AUTOSTART -eq '1') { $task.Trigger = New-ScheduledTaskTrigger -AtLogOn -User $user } $definition = New-ScheduledTask @task Register-ScheduledTask -TaskName $env:VASH_TASK -InputObject $definition -Force | Out-Null`, { VASH_TASK: taskName(options.instance), VASH_EXE: options.exe, VASH_ARGS: `task --instance ${options.instance}`, VASH_CWD: options.cwd, VASH_AUTOSTART: options.autostart ? '1' : '0' } ); } export async function runTask(instance: string) { await powershell(`Start-ScheduledTask -TaskName $env:VASH_TASK`, { VASH_TASK: taskName(instance) }); } export async function endTask(instance: string) { await powershell(`Stop-ScheduledTask -TaskName $env:VASH_TASK -ErrorAction SilentlyContinue`, { VASH_TASK: taskName(instance) }).catch(() => {}); } export async function unregisterTask(instance: string) { await powershell( `Unregister-ScheduledTask -TaskName $env:VASH_TASK -Confirm:$false -ErrorAction SilentlyContinue`, { VASH_TASK: taskName(instance) } ).catch(() => {}); } export interface TaskInfo { exists: boolean; state: string | null; lastRunTime: string | null; lastTaskResult: number | null; autostart: boolean; } export async function taskInfo(instance: string): Promise { const output = await powershell( `$task = Get-ScheduledTask -TaskName $env:VASH_TASK -ErrorAction SilentlyContinue if ($null -eq $task) { '{"exists":false}'; exit } $info = $task | Get-ScheduledTaskInfo @{ exists = $true; state = [string]$task.State; lastRunTime = if ($info.LastRunTime) { $info.LastRunTime.ToString('o') } else { $null }; lastTaskResult = [long]$info.LastTaskResult; autostart = @($task.Triggers).Count -gt 0 } | ConvertTo-Json -Compress`, { VASH_TASK: taskName(instance) } ); const info = JSON.parse(output) as Partial; return { exists: !!info.exists, state: info.state ?? null, lastRunTime: info.lastRunTime ?? null, lastTaskResult: info.lastTaskResult ?? null, autostart: !!info.autostart }; } /** Who is signed in at the PC's own screen, if anyone. */ export async function consoleUser() { const output = await powershell(`(Get-CimInstance Win32_ComputerSystem).UserName`).catch( () => '' ); return output || null; } export async function currentUser() { return powershell(`[System.Security.Principal.WindowsIdentity]::GetCurrent().Name`); } export async function gpuNames() { const output = await powershell( `(Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name) -join "\`n"` ).catch(() => ''); return output.split(/\r?\n/).filter(Boolean); }