Search code examples
powershellwebclientjobs

PowerShell & Net.WebClient. Need clarification on jobs behaviour


Lets say I am downloading large file using Net.WebClient's DownloadFile method:

$uri1 = "blabla.com/distro/blabla_2gb.exe"
$localfile1 = "$Env:userprofile\Downloads\blabla_2gb.exe"

$wbcl = New-Object System.Net.WebClient
$wbcl.DownloadFile($uri1, $localfile1)
$wbcl.Dispose()

In this case, I can terminate my script with something like Alt + F4 any moment. The downloading process will stop, and $wbcl will be disposed automatically.

But if I do the same thing inside of a job:

Start-Job -ScriptBlock `
{
  #SAME CODE AS ABOVE
} | Out-Null

#SOME PARALLEL ACTIVITY

Wait-Job -ID 1 | Out-Null

the donwloading continues, even when the parent script is closed. As per documentation, termination of the parent script will result in stopping of all of the corresponding jobs. Then why it continues downloading?

P.S. I know I can avoid starting a job here by using DownloadFileAsync, but I really eager to understand this mechanism :)


Solution

  • I believe this is because execution has flowed into a .NET method where PowerShell no longer has control of it.

    For example, if I run...

    Start-Job -ScriptBlock { Start-Sleep -Seconds 30 }
    

    ...or...

    Start-Job -ScriptBlock { while ($true) { } }
    

    ...I can see in Task Manager that there are two PowerShell processes. If I then click the close button of the PowerShell window (Alt + F4 doesn't work for me) both processes immediately disappear.

    If I run...

    Start-Job -ScriptBlock { [System.Threading.Thread]::Sleep([TimeSpan]::FromSeconds(30)) }
    

    ...then I also see two PowerShell processes in Task Manager. However, after closing the PowerShell window, only one of the PowerShell processes immediately disappears; the other disappears after the remainder of the 30 seconds. Interestingly, if I run exit instead of closing the PowerShell window, the window remains open with a blinking cursor until the job finishes.

    Another way to observe this is with Stop-Job. In this script...

    $job = Start-Job -ScriptBlock { Start-Sleep -Seconds 30 }
    Start-Sleep -Seconds 1 # Give the job time to transition to the Running state
    $job | Stop-Job
    

    ...Stop-Job returns immediately, whereas in this script...

    $job = Start-Job -ScriptBlock { [System.Threading.Thread]::Sleep([TimeSpan]::FromSeconds(30)) }
    Start-Sleep -Seconds 1 # Give the job time to transition to the Running state
    $job | Stop-Job
    

    ...it takes 30 seconds.

    I'm not too familiar with the low-level workings of PowerShell execution, but in the first two snippets when the parent process is closed the job process is running PowerShell code, so it will be able to interrupt at an arbitrary point and respond to the parent process's signal to terminate. In the third snippet, the job process is running .NET code, waiting for the method to return. I can't say if it's that the thread running the .NET code is the same thread that would communicate with the parent process or that it's a different thread and PowerShell is simply respecting the dangers of aborting another thread (that PowerShell has no problem interrupting DownloadFile() to exit when it's run outside of a job suggests the former), but the result is the same: the job process doesn't terminate because it's "stuck" inside .NET code until it completes.

    This might also be related to why Ctrl + C doesn't (immediately) work when executing a .NET method. See Powershell AcceptTcpClient() cannot be interrupted by Ctrl-C.

    One other point: make sure you call Dispose() inside a finally block to ensure it does get called even if DownloadFile() throws an exception...

    $wbcl = New-Object System.Net.WebClient
    try
    {
        $wbcl.DownloadFile($uri1, $localfile1)
    }
    finally
    {
        $wbcl.Dispose()
    }