Powershell invoke-computer to a remote that survives a reboot of my local computer?

976 Views Asked by At

Is there a way to "invoke-command" to a remote computer such that I can reboot my computer and the job will still be running, and I can check the output log whenever I want?

    PS> invoke-command -Computer Remote1 -File "ScriptThatRunsFor7days.ps1"
    PS> restart-computer
    PS> # Hey where's my job on remote computer?  Can i see it running and connect to
        # its output after rebooting my computer?
4

There are 4 best solutions below

0
On

Something with scheduled jobs. I'm copying the script to the remote computer using a pssession.

$s = new-pssession remote1
copy-item script.ps1 c:\users\admin\documents -tosession $s
invoke-command $s { Register-ScheduledJob test script.ps1 -Runnow }

And then later, only when it starts running, it will automatically appear as a regular job on the remote computer:

invoke-command remote1 { get-job | receive-job -keep }
0
On

Isn't it easier to just register a scheduled task that runs the script on the remote computer? For logging just use the cmdlet Start-Transcript at the top of the script. I made a script not to long ago to easely register scheduled tasks on a remote computer. Maybe you can try out and see if it works for you?

[CmdletBinding()]
param(
    [parameter(Mandatory=$true)]
    [string]
    $PSFilePath,

    [parameter(Mandatory=$true)]
    [string]
    $TaskName,

    [parameter(Mandatory=$true)]
    [string]
    $ComputerName
)

$VerbosePreference="Continue"

New-Variable -Name ScriptDestinationFolder -Value "Windows\PowershellScripts" -Option Constant -Scope Script

New-Variable -Name ScriptSourcePath -Value $PSFilePath -Option Constant -Scope Script
Write-Verbose "Script sourcepath: $ScriptSourcePath"

New-Variable -Name PSTaskName -Value $TaskName -Option Constant -Scope Script
Write-Verbose "TaskName: $TaskName"

$File = Split-Path $ScriptSourcePath -leaf
Write-Verbose "Filename: $File"
New-Variable -Name PSFileName -Value $File -Option Constant -Scope Script
Write-Verbose "PSFileName: $PSFileName"

$ExecutionTime = New-TimeSpan -Hours 8
Write-Verbose "Execution time: $ExecutionTime hours"

Invoke-Command -ComputerName $ComputerName -ScriptBlock {
    $VerbosePreference="Continue"        
    #Removing old Scheduled Task 
    Write-Verbose "Unregistering old scheduled task.."
    Stop-ScheduledTask -TaskName $Using:PSTaskName -ErrorAction SilentlyContinue
    Unregister-ScheduledTask -TaskName $Using:PSTaskName -Confirm:$false -ErrorAction SilentlyContinue


     #Creating destination directory for Powershell script
     $PSFolderPath = "C:" , $Using:ScriptDestinationFolder -join "\"
     Write-Verbose "Creating folder for script file on client: $PSFolderPath"
     New-Item -Path $PSFolderPath -ItemType Directory -Force

     #Scheduled Task definitions
    
     $Trigger = New-ScheduledTaskTrigger -Daily -At "8am" 
     $PSFilePath = "C:", $Using:ScriptDestinationFolder , $Using:PSFileName -join "\"  
     Write-Verbose "Setting path for script file to destination folder on client: $PSFilePath"
     $Action = New-ScheduledTaskAction -Execute PowerShell -Argument "-File $PSFilePath"
     $Principal = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\SYSTEM" -LogonType S4U
     $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -ExecutionTimeLimit $Using:ExecutionTime -StartWhenAvailable
     $Task = Register-ScheduledTask -TaskName $Using:PSTaskName -Principal $Principal -Action $Action -Settings $Settings -Trigger $Trigger
     $Task = Get-ScheduledTask -TaskName $Using:PSTaskName   
     $Task.Triggers[0].EndBoundary = [DateTime]::Now.AddDays(90).ToString("yyyyMMdd'T'HH:mm:ssZ") 
     Write-Verbose "Trigger expiration date set to: $Task.Triggers[0].EndBoundary"
     $Task.Settings.DeleteExpiredTaskAfter = 'P1D'
     Write-Verbose "Scheduled task will be deleted after $Task.Settings.DeleteExpiredTaskAfter after expiry."     
     $Task | Set-ScheduledTask -ErrorAction SilentlyContinue

 } #End Invoke-Command

#Copy script file from source to the computer
$ScriptDestination = "\" , $ComputerName , "C$",  $ScriptDestinationFolder  -join "\"
Write-Verbose "Script destination is set to: $ScriptDestination"

Write-Verbose "Copying script file: `"$ScriptSourcePath`" to `"$ScriptDestination`""
Copy-Item -Path $ScriptSourcePath -Destination $ScriptDestination -Force

Usage:

Create-ScheduledTask-Test.ps1 -ComputerName MyRemoteComputer -PSFilePath "ScriptToRun.ps1" -TaskName DoSomeWork
0
On

vonPryz provided the crucial pointer:

On Windows, PowerShell offers disconnected remote sessions that allow you to reconnect and collect output later, from any client session, even after a logoff or reboot - assuming that the disconnected session on the remote computer hasn't timed out.

See the conceptual about_Remote_Disconnected_Sessions help topic.

The following sample script demonstrates the approach:

  • Save it to a *.ps1 file and adapt the $computerName and $sessionName variable values.

  • The script assumes that the current user identity can be used as-is to remote into the target computer; if that is not the case, add a -Credential argument to the Invoke-Command and Get-PSSession calls.

  • Invoke the script and, when prompted, choose when to connect to the disconnected remote session that was created - including after a logoff / reboot, in which case the script is automatically reinvoked in order to connect to the disconnected session and retrieve its output.

  • See the source-code comments for details, particularly with respect to the idle timeout.

  • One aspect not covered below is output buffering: a disconnected session that runs for a long time without having its output retrieved can potentially accumulate a lot of output. By default, if the output buffer fills up, execution is suspended. The OutputBufferingMode session option controls the behavior - see the New-PSSessionOption cmdlet.

The gist of the solution is:

  • An Invoke-Command call with the -InDisconnectedSession switch that starts an operation on a remote computer in an instantly disconnected session. That is, the call returns as soon as the operation was started without returning any results from the operation yet (it returns information about the disconnected session instead).

  • A later Receive-PSSession call - which may happen after a reboot - implicitly connects to the disconnected session and retrieves the results of the operation.

$ErrorActionPreference = 'Stop'

# ADAPT THESE VALUES AS NEEDED
$computer = '???'            # The remote target computer's name.
$sessionName = 'ReconnectMe' # A session name of your choice.

# See if the target session already exists.
$havePreviousSession = Get-PSSession -ComputerName $computer -Name $sessionName

if (-not $havePreviousSession) {

  # Create a disconnected session with a distinct custom name 
  # with a command that runs an output loop indefinitely.
  # The command returns instantly and outputs a session-information object 
  # for the disconnected session.
  # Note that [int]::MaxValue is used to get the maximum idle timeout, 
  # but the effective value is capped by the value of the remote machine's 
  # MaxIdleTimeoutMs WSMan configuration item, which defaults to 12 hours.
  Write-Verbose -vb "Creating a disconnected session named $sessionName on computer $computer..."
  $disconnectedSession = 
    Invoke-Command -ComputerName $computer -SessionName $sessionName -InDisconnectedSession -SessionOption @{ IdleTimeout=[int]::MaxValue } { while ($true) { Write-Host -NoNewLine .; Start-Sleep 1 } }

  # Prompt the user for when to connect and retrieve the output 
  # from the disconnected session.
  do {
    $response = Read-Host @"
---

Disconnected session $sessionName created on computer $computer.

You can connect to it and retrieve its output from any session on this machine,
even after a reboot.

* If you choose to log off or reboot now, this script re-runs automatically 
  when you log back in, in order to connect to the remote session and collect its output.

* To see open sessions on the target computer on demand, run the following
  (append | Remove-PSSession to remove them):

   Get-PSSession -ComputerName $computer

---

Do you want to (L)og off, (R)eboot, (C)onnect right now, or (Q)uit (submit with ENTER)? [l/r/c/q] 
"@
  } while (($response = $response.Trim()) -notin 'l', 'r', 'c', 'q')


  $autoRelaunchCmd = { 
   Set-ItemProperty registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce 'ReconnectDemo' "$((Get-Command powershell.exe).Path) -noexit -command `"Set-Location '$PWD'; . '$PSCommandPath'`"" 
  }

  switch ($response) {
    'q' { Write-Verbose -vb 'Aborted.'; exit 2 }
    'c' { break } # resume below
    'l' { 
       Write-Verbose -vb "Logging off..."
       & $autoRelaunchCmd
       logoff.exe
       exit
    }
    'r' {
       Write-Verbose -vb "Rebooting..."
       & $autoRelaunchCmd
       Restart-Computer
       exit
     } 
  }

}

# Getting here means that a remote disconnection session was previously created.
# Reconnect and retrieve its output.

# Implicitly reconnect to the session by name,
# and receive a job object representing the remotely running command.
# Note: Despite what the docs say, -OutTarget Job seems to be the default.
#       Use -Output Host to directly output the results of the running command.
Write-Verbose -vb "Connecting to previously created session $sessionName on computer $computer and receiving its output..."
$job = Receive-PSSession -ComputerName $computer -Name $sessionName -OutTarget Job

# Get the output from the job, timing out after a few seconds.
$job | Wait-Job -Timeout 3
$job | Remove-Job -Force # Forcefully terminate the job with the indefinitely running command.

# Remove the session.
Write-Host
Write-Verbose -Verbose "Removing remote session..."
Get-PSSession -ComputerName $computer -Name $sessionName | Remove-PSSession
1
On
function remote_nohup {
    param(
        [string]$Machine,
        [string]$Cmd
    )       
    
        $job_tstamp     = $(get-date -f MMdd_HHmm_ss)
        $job_name       = "${job_tstamp}"
        $job_dir_start  = (Get-Location).Path
        $job_dir_sched  = "$env:userprofile/Documents/jobs"
        $job_file       = "${job_dir_sched}/${job_name}.run.ps1"
        $job_log        = "${job_dir_sched}/${job_name}.log"
        $job_computer   = $Machine
        $job_cmd        = $Cmd

        # Create Job File
        $job_ps1 = @(
            "`$ErrorActionPreference  = `"Stop`""
            ""
            "Start-Transcript -path $job_log -append"
            ""
            "try {"
            "    write-host 'job_begin:($job_name)'"
            "    "
            "    set-location $job_dir_start -EA 0"
            "    "
            "    write-host 'job_cmd:($job_cmd)'"
            "    $job_cmd | out-host"
            ""
            "    write-host 'job_end:($job_name)'"
            "}"
            "catch {"
            "    `$msg = `$_"
            "    write-host  `$msg"
            "    write-error `$msg"
            "}"
            "finally {"
            "    Stop-Transcript"
            "}"
            ""
            "Exit-PSSession"
        )
        
        try {
            New-Item -ItemType Directory -Force -EA:0 -Path $job_dir_sched | out-null
            copy-Item $remote_profile_ps1 $job_profile 
            
            write-host "Creating File: $job_file"
            $f1 = open_w $job_file -fatal
            foreach ($line in $job_ps1) {
                $f1.WriteLine($line)
            }           
        }
        finally {
            $f1.close()
        }   

        # Create Jobs Dir
        write-host "Creating remote job Directory"
        Invoke-Command -Computer $job_computer -ScriptBlock { 
            New-Item -ItemType Directory -Force -EA:0 -Path $using:job_dir_sched | out-null
        }
        
        # Copy Job File
        write-host "copy-Item -recurse -ToSession `$s2  $job_file $job_file"
        $s2 = New-PSSession -Computer $job_computer
        copy-Item -ToSession $s2  $job_file $job_file    
        Receive-PSSession -Session $s2
        Remove-PSSession -Session $s2

        # Create Persistent Background Job
        write-host "Submitting job to remote scheduler"
        Invoke-Command -Computer $job_computer -ScriptBlock {
            Register-ScheduledJob -RunNow -Name $using:job_name -File $using:job_file
            Exit-PSSession          
        }

        # NOTE: Log file from run is placed on 
        #    remote computer under jobs dir     
}

function open_w {
    param([string]$path, [switch]$fatal)

    try {
        write-host "path: $path"  
        $pathfix = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
        $handle = [System.IO.StreamWriter]::new($pathfix, $false) #false:over-write, true:append
    }
    catch {
        if ($fatal) {
            write-host -ForegroundColor Red "EXCEPTION: " + $PSItem.ToString()      
            exit 1
        }
        return $null
    }
    return $handle
}