
I have written a code, which is opening a browser and taking a screenshot. But when I running it through remote desktop, it's taking blank image.
Can anyone suggest, how can I take the screenshot of browser on the remote desktop through PowerShell ?
for e.g. I need to open https://stackoverflow.com/ on remote desktop and save screenshot in that remote server.
Code:
[Reflection.Assembly]::LoadWithPartialName("System.Drawing") function screenshot([Drawing.Rectangle]$bounds, $path) { $bmp = New-Object Drawing.Bitmap $bounds.width, $bounds.height $graphics = [Drawing.Graphics]::FromImage($bmp) $graphics.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.size) $bmp.Save($path) $graphics.Dispose() $bmp.Dispose() }
101 Answer
What you are trying to do can't be directly performed remotely
When I say "remotely" here, I mean you can't invoke the screen capture from one system to another. The code has to be executed in a local context.
Ultimately, you'll need to run $graphics.CopyFromScreen()
from a user who is also logged into the Desktop, not just over a PSRemoting
session. There's literally nothing GUI-wise to capture; the remoting session has no GUI components loaded on the other side. In short, you can't expect to use PSRemoting
to capture a remote screenshot. It's not happening, there is no screen to copy from, and should result in a Win32Exception
: "The handle is invalid". This occurs any time you attempt to copy the screen buffer (screen buffer is different than the display buffer) from a PSRemoting
session, even if the user is logged in via RDP at the same time.
Note: My testing indicates the screen capture should still work over RDP without modification to your existing code. I am not sure whether there are GPOs or other settings which can prevent this from working.
However, if the logon session exists but is inactive (e.g. RDP client disconnected), you will get the same "Invalid handle" error. This answer sheds some more light on the behavior, but basically you need to have an active user session to have the draw context copied from. Without this, there is no draw context and thus there is no valid handle to obtain the graphics data from.
There is a possible solution using the Win32 API to perform an interactive logon as another user and then execute your PowerShell script (also with a Win32 API call), I have more information about that further in this answer.
Workaround as long as the target user session is in an active state
To work around this issue, you will have to impersonate a logged on Desktop principal with an active session and run your script in a local context. If you use PSRemoting
to establish a remote connection, set up a scheduled task to run a script to capture the screen as a targeted logged-in user (your script should also write the screenshot to a temp file somewhere):
Note: If there are multiple users you might need to capture the desktop of, you will need a task per user or reconfigure the task for the target user prior to running it.
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-File C:\path\to\ScreenShotScript.ps1" $trigger = New-ScheduledTaskTrigger -Once $principal = "DomainOrComputerName\AccountName" $settings = New-ScheduledTaskSettingsSet $task = New-ScheduledTask -Action $action -Principal $principal -Trigger $trigger -Settings $settings Register-ScheduledTask TakeScreenShot -InputObject $task
then just invoke the task when you want to run it:
Start-ScheduledTask -TaskName TakeScreenShot while( ( Get-ScheduledTask -TaskName TakeScreenShot ).State -ne 'Ready' ) { # Wait until the screencap task finishes Start-Sleep 1 }
Then from your host session you are remoting from (note that your session should be stored as a variable for this copy to work):
Copy-Item -FromSession $psRemotingSession C:\path\to\screenshot.png C:\path\to\save\screenshot\locally\to
If your end goal is to also connect to RDP and launch the site with PowerShell, I have the following function I can share to initiate a remote RDP connection via PowerShell. You can then use the Task Scheduler workaround above to launch a browser to the correct site as well as perform your remote capture, though you could also just screenshot the RDP window as well before closing it:
Note: This function returns $True
if mstsc
was launched but does not check the result of a successful connection. It will return $False
if the credential was unable to be stored for some reason.
function Connect-RDP { Param( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$Hostname, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ushort]$Port, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [pscredential]$Credential ) $cmdkeyArgs = "/generic:TERMSRV/${Hostname}", "/user:$($Credential.Username)", "/pass:$($Credential.GetNetworkCredential().Password)" cmdkey @cmdkeyArgs if ( $LASTEXITCODE -ne 0 ) { throw "cmdkey failed with exit code ${LASTEXITCODE}" } $mstscArgs = , "/v:${Hostname}:${Port}" mstsc @mstscArgs }
This function can also be used to start an active RDP session to the target machine with the target user, so your existing screenshot code would work. Just remember to close the window when you are done. Note this would kick out the target user if they are already logged on remotely or locally.
You could also theoretically manipulate the browser window via code run through Task Scheduler, but you would probably do better to manipulate the browser window through the RDP window from the local session unless you are relying on something like Selenium
.
The answer I linked to above also offers some additional workarounds you can try, including additional User32
functions from the Win32
API which you can P/Invoke
to access. However, these workarounds may force a re-architecture of your current automation solution. Note that this answer is from 2012 and the suggestion to capture the screen buffer from a service may no longer apply as the ability to grant services access to a user's desktop was quietly revoked during the lifetime of Windows Vista.
Additional information
You can see how CopyFromScreen
works internally from the .NET Core source as well as the User32.GetDC(IntPtr)
function it uses to understand this behavior better.
If you want to give AdvApi.CreateProcessAsUser
from the Win32 API a shot, I have written a function that will P/Invoke
the necessary Win32 API functions and create the dependency data structures within your current PowerShell session. Unfortunately I've had some difficulty getting LogonUser
to work so I don't have a working example of actually performing the logon and starting a process as another user, but this function at least does the work in getting the functions available in your PowerShell session.
Here's how to use the function once it's defined in your session:
# This function will only work once per session (assuming Add-Type doesn't errorout ) PInvoke-AdvApi32 # I don't have a working example but call both functions as static methods on `[AdvApi32]` like so [AdvApi32]::LogonUser(....) [AdvApi32]::CreateProcessAsUser(....)
For proper examples of how to use these functions, you can reference the P/Invoke
documentation for CreateProcessAsUser and LogonUser. Note that the samples are in C# and VB.NET so you will have to transpose the examples to .NET yourself. If you can get LogonUser
working, you should be able to invoke your PowerShell code as a new process for the target user even when going through a remoting connection.
There may be other AdvApi32
functions that are of use to you if you attempt this route, if you decide to P/Invoke
additional methods note that you can paste into the C# code definition in the function, making sure to change any access modifiers to public
(they are often defined as internal
in the C# samples). Any additional data structures you also need to define will be mentioned on any of the P/Invoke
pages on that site.
In Summarium
User32
's GetDC(IntPtr)
function gets the drawing context of the specified window handle, or the whole desktop if 0
is provided. This function behavior is critical for Graphics.CopyFromScreen
to function correctly. The main issue, as mentioned further above, is that there is no drawing context when you connect via PSRemoting
, nor will there be one if you use the Task Scheduler workaround when the logon session is not in an active state. So whether you use Graphics.CopyFromScreen
or attempt to rewrite some of what you find in the .NET Core source linked above, you cannot copy the screen buffer of a user if:
- The user is logged out.
- The user is logged in but the session is inactive (e.g. disconnected RDP client, locked screen, etc).
- The user session is active but you are attempting to copy the screen buffer over a
PSRemoting
session asPSRemoting
does not give the remote user access to graphical contexts on the remote machine. - There is a potential solution with the Win32 API, but it is not for the faint of heart.
PInvoke-AdvApi32 Function
Here is the source for PInvoke-AdvApi32
:
Function PInvoke-AdvApi32 { Param( [string]$ExposedClassName = 'AdvApi32', [switch]$PassThru ) #region CSDefinitions # Define the C# code we need to import # ... yes this function needs several definitions $pinvokeDefinitions = @" using System; using System.Runtime.InteropServices; [Flags] public enum CreateProcessFlags { CREATE_BREAKAWAY_FROM_JOB = 0x01000000, CREATE_DEFAULT_ERROR_MODE = 0x04000000, CREATE_NEW_CONSOLE = 0x00000010, CREATE_NEW_PROCESS_GROUP = 0x00000200, CREATE_NO_WINDOW = 0x08000000, CREATE_PROTECTED_PROCESS = 0x00040000, CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000, CREATE_SEPARATE_WOW_VDM = 0x00000800, CREATE_SHARED_WOW_VDM = 0x00001000, CREATE_SUSPENDED = 0x00000004, CREATE_UNICODE_ENVIRONMENT = 0x00000400, DEBUG_ONLY_THIS_PROCESS = 0x00000002, DEBUG_PROCESS = 0x00000001, DETACHED_PROCESS = 0x00000008, EXTENDED_STARTUPINFO_PRESENT = 0x00080000, INHERIT_PARENT_AFFINITY = 0x00010000 } public enum LOGON_PROVIDER { LOGON32_PROVIDER_DEFAULT, LOGON32_PROVIDER_WINNT35, LOGON32_PROVIDER_WINNT40, LOGON32_PROVIDER_WINNT50 } public enum LOGON_TYPE { LOGON32_LOGON_INTERACTIVE = 2, LOGON32_LOGON_NETWORK = 3, LOGON32_LOGON_BATCH = 4, LOGON32_LOGON_SERVICE = 5, LOGON32_LOGON_UNLOCK = 7, LOGON32_LOGON_NETWORK_CLEARTEXT = 8, LOGON32_LOGON_NEW_CREDENTIALS = 9 } // This also works with CharSet.Ansi as long as the calling function uses the same character set. [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct STARTUPINFOEX { public STARTUPINFO StartupInfo; public IntPtr lpAttributeList; } // If you are using this with [GetStartupInfo], this definition works without errors. [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct STARTUPINFO { public Int32 cb; public IntPtr lpReserved; public IntPtr lpDesktop; public IntPtr lpTitle; public Int32 dwX; public Int32 dwY; public Int32 dwXSize; public Int32 dwYSize; public Int32 dwXCountChars; public Int32 dwYCountChars; public Int32 dwFillAttribute; public Int32 dwFlags; public Int16 wShowWindow; public Int16 cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } [StructLayout(LayoutKind.Sequential)] public struct SECURITY_ATTRIBUTES { public int nLength; public unsafe byte* lpSecurityDescriptor; public int bInheritHandle; } public enum LogonProvider { /// <summary> /// Use the standard logon provider for the system. /// The default security provider is negotiate, unless you pass NULL for the domain name and the user name /// is not in UPN format. In this case, the default provider is NTLM. /// NOTE: Windows 2000/NT: The default security provider is NTLM. /// </summary> LOGON32_PROVIDER_DEFAULT = 0, LOGON32_PROVIDER_WINNT35 = 1, LOGON32_PROVIDER_WINNT40 = 2, LOGON32_PROVIDER_WINNT50 = 3 } public class ${ExposedClassName} { [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)] public static extern bool CreateProcessAsUser( IntPtr hToken, string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("advapi32.dll", SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool LogonUser( [MarshalAs(UnmanagedType.LPStr)] string pszUserName, [MarshalAs(UnmanagedType.LPStr)] string pszDomain, [MarshalAs(UnmanagedType.LPStr)] string pszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken); } "@ #endregion CSDefinitions # Compile and load our heroic Win32 helper class and definitions if ( !( [System.Management.Automation.PSTypeName]$ExposedClassName ).Type ) { Write-Host "Adding type ""${ExposedClassName}""" $addTypeParams = @{ TypeDefinition = $pinvokeDefinitions CompilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters -Property @{ CompilerOptions = '/unsafe' } PassThru = $PassThru ErrorAction = 'Stop' } Add-Type @addTypeParams } else { Write-Warning "AdvApi32 has already been P/Invoked. If you need to P/Invoke this class again function again, you must start a new PowerShell session." Write-Warning "Changing the -ExposedClassName will bypass this check but Add-Type will fail on dependent definitions without a unique name." } }
2ncG1vNJzZmirpJawrLvVnqmfpJ%2Bse6S7zGiorp2jqbawutJobXJoZG6CeH6OoaawZaSkerWtyp5krJuimrKvv8eoq2annmK%2FprnOrZxmnJWouLW7z2asrKGenHqxu9aeqayglaG5