Running setup in background with msdeploy - powershell

We're deploying build directly from TFS build to servers with msdeploy. There are many components and some of those need setup (multiple nservicebus services that take ~1min each to start/install/stop). Also this "gateway" server runs msdeploy itself to "fanout" deployment binaries to other servers that run similar setup tasks depending on their role.
It doesn't make sense for build server to be hung up on this task for a long time so we need some way to make this setup run in "background"/detached from build server perspective.
We're using powershell as deployment script engine.
So here is what we've tried:
Jobs started with start-job are killed as soon as original msdeploy -started powershell exits.
Jobs started with start-process hang up the msdeploy connection until they finish
We can't loop with invoke-command -ComputerName localhost -InDisconnectedSession (this worked from shell on same machine but not through msdeploy; I don't care for passing credentials around because we don't have DA creds and deployed machines are in multiple networks)
I post my solution below, I ended up using scheduled task that is launched immediately. But I hope somebody would know a cleaner solution.

This program will schedule itself to run immediately, this can be "triggered" with msdeploy runCommand and not block output.
This is our msdeploy using trigger script:
param([string]$dest = "fully.qualified.example.com",
[string]$serviceMachine = "windowsmachine",
[string]$deployUser = "webdeploy",
[string]$deployPass = "***",
[string]$localPath = "c:/temp/source",
[string]$remotePath = "c:/temp"
);
$deployCommand = "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe"
$depArgs = #('-verb:sync'
"-source:dirpath=$localPath"
"-dest:computerName=https://${dest}/msdeploy.axd,userName=$servicemachine\$deployUser,password=$deployPass,authType=basic,dirPath=$remotePath"
"-postSync:runCommand='powershell -inputformat none $remotePath/starter.ps1',waitInterval=30000"
"-enableRule:DoNotDeleteRule"
"-allowUntrusted")
$command = "& '$deployCommand' --% $depArgs"
& $ExecutionContext.InvokeCommand.NewScriptBlock($command)
and here is the starter.ps1 triggered in above:
param([switch] $fg)
$scheduledJobName = "starter_v1"
$localPath = (Get-Location).Path
$logfile = "c:\temp\starterlog.txt"
function startBGJob() {
try {
Get-ScheduledJob -Name $scheduledJobName -ErrorAction Stop
"[BGJ] Found job $scheduledJobName, starting."
# no way in PS to start the job as actual scheduled task
schtasks /run /tn "\Microsoft\Windows\PowerShell\ScheduledJobs\$scheduledJobName"
} catch {
"[BGJ] Job not found, creating and starting new"
$jobsb = {
param($lp, $logfile);
& "$lp\starter.ps1" -fg | out-file $logfile
}
$option = New-ScheduledJobOption -RunElevated
Register-ScheduledJob -name $scheduledJobName -ScriptBlock $jobsb -ArgumentList ($localPath, $logfile) -ScheduledJobOption $option -RunNow
}
}
function Main() {
if ($fg -eq $true) {
# actual work done here
} else {
startBGJob
}
}
Main
I had to extract some things from those, so typos may be within.

Related

PowerShell Remoting, Eventing, Runspace - sticky Event Subscriber

I am currently stuck at a problem involving a WMI Event subscriber on a remote session running in a background runspace.
Below is the representative code that reproduces the issue (the real code is too long to share here) but, essentially, through a PS script I remotely install advertised WSUS updates with reboots when necessary. It takes about 90 minutes end to end.
The issue I am trying to solve at the moment is that during the course of patching, the support staff inadvertently log in to the the server being remotely patched and do their admin activities. To remind them, I want to display a pop up message as soon as the user logs in when my remote script is running. I am trying to do it using a background runspace plugged into the main patching script. It makes use of WMI eventing on the target server (which is being patched) to monitor user logons and display message as soon as it detects one. The below code is working as I expect. It even survives target server reboots.
$RemoteServerName = 'Server1.contoso.com'
$UserLogonAlertScriptBlock = {
param ($SyncedHashTable)
$RemoteServerName = $SyncedHashTable.target
try {
$Session = New-PSSession -ComputerName $RemoteServerName -ErrorAction Stop
} catch {}
while($true){
if($Session.State -eq 'Opened') {
$RemoteMonitoringJob = Invoke-Command -Session $Session -AsJob -ScriptBlock {
$null = Register-WMIEvent -Query "SELECT * FROM __InstanceCreationEvent WITHIN 3 WHERE TargetInstance ISA 'Win32_LogonSession'" -SourceIdentifier 'User.Logon'
Wait-Event -SourceIdentifier "User.Logon" -Timeout 7200 | ForEach-Object {
msg * /TIME:7200 /V "User logon detected" | Out-Null
$_ | Remove-Event
}
}
while($RemoteMonitoringJob.State -in #('NotStarted','Running')) {
Start-Sleep -Seconds 1
}
} else {
while($true) {
try {
$Session = New-PSSession -ComputerName $RemoteServerName -ErrorAction Stop
} catch {}
if($Session.State -eq 'Opened') {
break
}
Start-Sleep -Seconds 1
}
}
}
}
$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.runspace = $Runspace
$SyncedHashTable = [hashtable]::Synchronized(#{})
$SyncedHashTable.host = $host
$SyncedHashTable.target = $RemoteServerName
$Runspace.Open()
$handle = $PowerShell.AddScript($UserLogonAlertScriptBlock).AddArgument($SyncedHashTable).BeginInvoke()
Write-Host '$(Get-Date): Long running script execution targeting $RemoteServerName has started'
Start-Sleep -Seconds 120 # it usually runs for upto 90 minutes, with remote reboots of $RemoteServerName
Write-Host "$(Get-Date): The script execution has completed"
### The code that cleans up the sticky event subscriber on the target server needs to be added here
The part I am stuck at is after the script completes its execution. The wsmanprovhost.exe running on the target server continues to stick around and shows alert messages when new users log on. I think it's because of the WMI event listener still being active on the box, not releasing the remote PS session.
In the above code, I need help close that remote listener so wsmanprovhost.exe disappears.
Could you please help?
PS. I have referred to #mklement0 's response in the following post but still no joy: The RunSpace and its closure
Update:
I have managed to address the challenge by adding a Boolean flag into the SyncedHashtable which is passed to the background runspace. When I want to stop the remote logon monitoring, in the main script I flip the flag. Since it's inside a synced hashtable, I can monitor that inside the runspace and terminate the remote invoke command job in the run space. But I still had to forcibly kill the remote wsmprovhost.exe as it refuses to go. I could do it by getting the pid of the remote PS session in advance. Not the most elegant way to close a remote PS session but it does the job for me. It's just that since the remote session is continuously monitoring for user logon event, there does not appear to be a way to run a piece of code in that session to unsubscribe the WMI event source. Will do more testing to see if there is any side effect.

Powershell - get string information to a console for user from one script to another

I have 2 scripts, one serves as a Worker - uses variables to complete commands, second serves as a function Library. Worker via variable loads Library and uses functions from there.
As a user, when I run the script I would like to see the output in the console which I defined as the outcome for Library script.
Example Worker script:
Param(
[string]$server_01,
[string]$releaseDefinitionName,
[string]$pathRelease,
[string]$buildNumber,
[string]$command_01,
[string]$scheduledTask_01
)
$pathScriptLibrary = $pathRelease + "\" + $buildNumber + "\" + "_scripts"
. $pathScriptLibrary\_library.ps1
$user = xxx
$password = xxx
$cred = xxx
Invoke-Command -ComputerName $server_01 -Credential $cred -ErrorAction Stop -ScriptBlock {powershell $command_01}
Example Library script:
function Stop-ScheduledTasks{
Write-Output [INFO]: Stopping scheduled tasks... -ForegroundColor White
Get-ScheduledTask -TaskName "$scheduledTask_01" | ForEach {
if ($_.State -eq "Ready") {
Write-Output [WARNING]: Scheduled task $scheduledTask_01 was already stopped. -ForegroundColor Yellow
}
else {
Stop-ScheduledTask -TaskName "$scheduledTask_01"
Write-Output [OK]: Running task $scheduledTask_01 stopped. -ForegroundColor Green
}
}
}
function Start-ScheduledTasks{
Write-Output [INFO]: Starting scheduled tasks... -ForegroundColor White
Get-ScheduledTask -TaskName "$scheduledTask_01" | ForEach {
if ($_.State -eq "Running") {
Write-Output [WARNING]: Scheduled task $scheduledTask_01 already started. -ForegroundColor Yellow
}
else {
Start-ScheduledTask -TaskName "$scheduledTask_01"
Write-Output [OK]: Stopped scheduled task $scheduledTask_01 started. -ForegroundColor Green
}
}
}
Use case:
User starts the deployment by clicning the deploy button in Azure DevOps UI
The task using the Worker script takes function from Library script (in this case stops Scheduled Task) and performs it
User checks log on the Azure DevOps side and sees the custom output lines from Library script. (2 of them now - 1. starting with [INFO], 2. either starting with [WARNING] or [OK]).
Could you please advice a solution how to achieve that? Thank you.
NOTE: Those examples are run in Azure DevOps (on premise) release pipelines and desired outcomes are ment for users running those pipelines.
If you're trying to write to the azure devops pipeline log, then you should avoid using Write-Output. That does something subtly different; it adds to the function's return value.
So for example the Write-Output in the function Stop-ScheduledTask; that is roughly equivalent to you putting at the end of the function:
return "[WARNING]: Scheduled task $scheduledTask_01 was already stopped."
That might end up being printed to the pipeline log, or it might not; and importantly, it might completely mess up a function which is genuinely trying to return a simple value.
Instead of using Write-Output, I recommend using Write-Host. What that does is immediately write a line to the pipeline log, without affecting what a library function will return.
Write-Output "[WARNING]: Scheduled task $scheduledTask_01 was already stopped."
You can also use Write-Warning and Write-Error.

Why don't the applications run by Powershell appear on remote desktop but appear in task manager? [duplicate]

I've created a pssession on a remote computer and entered that possession. From within that session I use start-process to start notepad. I can confirm that notepad is running with the get-process command, and also with taskmgr in the remote computer. However, the GUI side of the process isn't showing. This is the sequence I've been using:
$server = New-PSSession -ComputerName myserver -Credential mycreds
Enter-PSSession $server
[$server]: PS C:\>Start-Process notepad -Wait -WindowStyle Maximized
The process is running, but while RDP'd to the box, notepad does not open. If I open notepad from the server, a new notepad process begins. I also tried by using the verb parameter like this:
[$server]: PS C:\>Start-Process notepad -Wait -WindowStyle Maximized -Verb Open
Same result tho... Process starts, but no notepad shows. I've tried this while remoted into the box (but issued from my local host) as well as before remoting into the server.
That is because your powershell session on the remote machine does not go to any visible desktop, but to an invisible system desktop. The receiving end of your powershell remote session is a Windows service. The process is started, but nor you nor anyone else can ever see it.
And if you think about it, since multiple users could RDP to the same machine, there is really no reason to assume a remote powershell session would end up showing on any of the users desktops. Actually, in almost all cases you wouldn't want it anyway.
psexec with the -i parameter is able to do what you want, but you have to specify which of the sessions (users) you want it to show up in.
I know this is old, but I came across it looking for the solution myself so I wanted to update it for future poor souls.
A native workaround for this problem is to use a scheduled task. That will use the active session
function Start-Process-Active
{
param
(
[System.Management.Automation.Runspaces.PSSession]$Session,
[string]$Executable,
[string]$Argument,
[string]$WorkingDirectory,
[string]$UserID
)
if (($Session -eq $null) -or ($Session.Availability -ne [System.Management.Automation.Runspaces.RunspaceAvailability]::Available))
{
$Session.Availability
throw [System.Exception] "Session is not availabile"
}
Invoke-Command -Session $Session -ArgumentList $Executable,$Argument,$WorkingDirectory,$UserID -ScriptBlock {
param($Executable, $Argument, $WorkingDirectory, $UserID)
$action = New-ScheduledTaskAction -Execute $Executable -Argument $Argument -WorkingDirectory $WorkingDirectory
$principal = New-ScheduledTaskPrincipal -userid $UserID
$task = New-ScheduledTask -Action $action -Principal $principal
$taskname = "_StartProcessActiveTask"
try
{
$registeredTask = Get-ScheduledTask $taskname -ErrorAction SilentlyContinue
}
catch
{
$registeredTask = $null
}
if ($registeredTask)
{
Unregister-ScheduledTask -InputObject $registeredTask -Confirm:$false
}
$registeredTask = Register-ScheduledTask $taskname -InputObject $task
Start-ScheduledTask -InputObject $registeredTask
Unregister-ScheduledTask -InputObject $registeredTask -Confirm:$false
}
}
When you use New-PSSession and then RDP into that same computer, you're actually using two separate and distinct user login sessions. Therefore, the Notepad.exe process you started in the PSSession isn't visible to your RDP session (except as another running process via Task Manager or get-process).
Once you've RDP'd into the server (after doing what you wrote in your post), start another Notepad instance from there. Then drop to PowerShell & run this: get-process -name notepad |select name,processid
Note that there are two instances, each in a different session.
Now open up Task Manager and look at the user sessions. Your RDP session will probably be listed as session 1.
Now quit Notepad and run get-process again. You'll see one instance, but for session 0. That's the one you created in your remote PSSession.
There are only 2 workarounds that I know of that can make this happen.
Create a task schedule as the logged in user, with no trigger and trigger it manually.
Create a service that starts the process with a duplicated token of the logged in user.
For the task schedule way I will say that new-scheduledtask is only available in Windows 8+. For windows 7 you need to connect to the Schedule Service to create the task like this (this example also starts the task at logon);
$sched = new-object -ComObject("Schedule.Service")
$sched.connect()
$schedpath = $sched.getFolder("\")
$domain = "myDomain"
$user="myuser"
$domuser= "${domain}\${user}"
$task = $sched.newTask(0) # 0 - reserved for future use
$task.RegistrationInfo.Description = "Start My Application"
$task.Settings.DisallowStartIfOnBatteries=$false
$task.Settings.ExecutionTimeLimit="PT0S" # there's no limit
$task.settings.priority=0 # highest
$task.Settings.IdleSettings.StopOnIdleEnd=$false
$task.settings.StopIfGoingOnBatteries=$false
$trigger=$task.Triggers.create(9) # 9 - at logon
$trigger.userid="$domuser" # at logon
$action=$task.actions.create(0) # 0 - execute a command
$action.path="C:\windows\system32\cmd.exe"
$action.arguments='/c "c:\program files\vendor\product\executable.exe"'
$action.WorkingDirectory="c:\program files\vendor\product\"
$task.principal.Id="Author"
$task.principal.UserId="$domuser"
$task.principal.LogonType=3 # 3 - run only when logged on
$task.principal.runlevel=1 # with elevated privs
# 6 - TASK_CREATE_OR_UPDATE
$schedpath.RegisterTaskDefinition("MyApplication",$viztask,6,$null,$null,$null)
Creating a service is way more complicated, so I'll only outline the calls needed to make it happen. The easy way is to use the invoke-asservice script on powershell gallery: https://www.powershellgallery.com/packages/InvokeAsSystem/1.0.0.0/Content/Invoke-AsService.ps1
Use WTSOpenServer and WTSEnumerateSessions to get the list of sessions on the machine. You also need to use WTSQuerySessionInformation on each session to get additional information like username. Remember to free your resources using WTSFreeMemory and WTSCloseServer You'll end up with some data which looks like this (this is from the qwinsta command);
SESSIONNAME USERNAME ID STATE
services 0 Disc
>rdp-tcp#2 mheath 1 Active
console 2 Conn
rdp-tcp 65536 Listen
Here's an SO post about getting this data; How do you retrieve a list of logged-in/connected users in .NET?
This is where you implement your logic to determine which session to target, do you want to display it on the Active desktop regardless of how it's being presented, over RDP or on the local console? And also what will you do if there is no one logged on? (I've setup auto logon and call a lock desktop command at logon so that a logged in user is available.)
You need to find the process id of a process that is running on the desktop as that user. You could go for explorer, but your machine might be Server Core, which explorer isn't running by default. Also not a good idea to target winlogon because it's running as system, or dwm as it's running as an unprivileged user.
The following commands need to run in a service as they require privileges that only system services have. Use OpenProcess to get the process handle, use OpenProcessToken to get the security token of the process, duplicate the token using DuplicateTokenEx then call ``CreateProcessAsUser``` and finally Close your handles.
The second half of this code is implemented in invoke-asservice powershell script.
You can also use the sysinternals tool psexec, I didn't list it as a 3rd way because it just automates the process of creating a service.

Install Windows HotFix using Terraform on AWS

I have a very simple PowerShell script that uploads a generated test file to an AWS S3 bucket from a Windows 2008 R2 Datacenter server (clean AWS instance). If I run the script remotely on the server using Terraform (remote-exec provisioner), the script fails on the S3 upload with a StackOverflowException. When I run the script directly on the server, it runs fine and uploads the file.
I've experimented with different sizes for the file and 14.5MB seems to be about the maximum that works before the StackOverflowException occurs. Just about any size works fine when I RDP into the server and run the script directly. I've tested 200MB and it works fine.
Any idea why this is happening or what I can do to fix it? The actual file I need to upload is 50MB.
Here are the essential parts to recreate the problem. terraform.tf file:
resource "aws_instance" "windows" {
count = "1"
ami = "ami-e935fc94" #base win 2008 R2 datacenter
instance_type = "t2.micro"
connection {
type = "winrm"
user = "<username>"
password = "<password>"
timeout = "30m"
}
provisioner "file" {
source = "windows/upload.ps1"
destination = "C:\\scripts\\upload.ps1"
}
provisioner "remote-exec" {
inline = [
"powershell.exe -File C:\\scripts\\upload.ps1"
]
}
}
The PowerShell script is very simple. upload.ps1:
$f = new-object System.IO.FileStream C:\Temp\test.dat, Create, ReadWrite
$f.SetLength(40MB) # change this to 14.5MB and it works!
$f.Close()
Write-S3Object -BucketName "mybucket" -Folder "C:\Temp" -KeyPrefix "20180322" -SearchPattern "*.dat"
The error that I receive when launching the script from Terraform (remote-exec provisioner):
aws_instance.windows (remote-exec): Process is terminated due to StackOverflowException.
Running upload.ps1 from RDP on the server itself works fine, including larger files (tested up to 200MB).
Here is the version information:
Microsoft Windows Server 2008 R2 Datacenter
Powershell Version: 3.0
AWS Tools for Windows PowerShell, Version 3.3.245.0
Amazon Web Services SDK for .NET, Core Runtime Version 3.3.21.15
This problem results from a Windows bug. This is all fine and good for a standard Windows server -- you can patch and move on. But, things are more tricky with AWS automation using Terraform.
The ideal solution would allow 1) use of the base AMI, 2) apply the hotfix to itself, and 3) then run the WinRM remote-exec, all from Terraform. Another solution would be to create an AMI with the hotfix installed and have Terraform generate instances using that AMI. However, then you're stuck maintaining AMIs.
Normally, I grab the Microsoft-provided base AMI using a filter:
data "aws_ami" "windows2008" {
most_recent = true
filter {
name = "virtualization-type"
values = ["hvm"]
}
filter {
name = "name"
values = ["Windows_Server-2008-R2_SP1-English-64Bit-Base*",]
}
owners = ["801119661308", "amazon"]
}
Then I use that AMI to create the AWS instance:
resource "aws_instance" "windows" {
count = "1"
ami = "${data.aws_ami.windows2008.id}"
...
}
But, the base AMI doesn't have the hotfix installed allowing you to avoid this WinRM/Windows bug. This is were it gets tricky.
You can use a userdata script to perform a multi-phase setup. In the first boot of the instance (Phase 1), we'll block the instance so that the remote-exec doesn't come in before we're ready. Then, we'll download and install the hotfix and we'll reboot (thanks to Niklas Akerlund, Micky Balladelli and Techibee). On the second boot (in method described here), we'll unblock the instance (enable WinRM) so that the remote-exec can connect.
Here's my userdata/PowerShell script:
$StateFile = "C:\Temp\userdata_state.txt"
If(-Not (Test-Path -Path $StateFile))
{
# PHASE 1
# Close the instance to WinRM connections until instance is ready (probably already closed, but just in case)
Start-Process -FilePath "winrm" -ArgumentList "set winrm/config/service/auth #{Basic=`"false`"}" -Wait
# Set the admin password for WinRM connections
$Admin = [adsi]("WinNT://./Administrator, user")
$Admin.psbase.invoke("SetPassword", "${tfi_rm_pass}")
# Create state file so after reboot it will know
New-Item -Path $StateFile -ItemType "file" -Force
# Make it so that userdata will run again after reboot
$EC2SettingsFile="C:\Program Files\Amazon\Ec2ConfigService\Settings\Config.xml"
$Xml = [xml](Get-Content $EC2SettingsFile)
$XmlElement = $Xml.get_DocumentElement()
$XmlElementToModify = $XmlElement.Plugins
Foreach ($Element in $XmlElementToModify.Plugin)
{
If ($Element.name -eq "Ec2HandleUserData")
{
$Element.State="Enabled"
}
}
$Xml.Save($EC2SettingsFile)
# Download and install hotfix
# Download self-extractor
$DownloadUrl = "https://hotfixv4.trafficmanager.net/Windows%207/Windows%20Server2008%20R2%20SP1/sp2/Fix467402/7600/free/463984_intl_x64_zip.exe"
$HotfixDir = "C:\hotfix"
$HotfixFile = "$HotfixDir\KB2842230.exe"
mkdir $HotfixDir
(New-Object System.Net.WebClient).DownloadFile($DownloadUrl, $HotfixFile)
# Extract self-extractor
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($HotfixFile, $HotfixDir)
# Install - NOTE: wusa returns immediately, before install completes, so you must check process to see when it finishes
Get-Item "$HotfixDir\*.msu" | Foreach { wusa ""$_.FullName /quiet /norestart"" ; While (#(Get-Process wusa -ErrorAction SilentlyContinue).Count -ne 0) { Start-Sleep 3 } }
# Reboot
Restart-Computer
}
Else
{
# PHASE 2
# Open WinRM for remote-exec
Start-Process -FilePath "winrm" -ArgumentList "quickconfig -q"
Start-Process -FilePath "winrm" -ArgumentList "set winrm/config/service #{AllowUnencrypted=`"true`"}" -Wait
Start-Process -FilePath "winrm" -ArgumentList "set winrm/config/service/auth #{Basic=`"true`"}" -Wait
Start-Process -FilePath "winrm" -ArgumentList "set winrm/config #{MaxTimeoutms=`"1900000`"}"
}

Powershell Using Start-Process in PSSession to Open Notepad

I've created a pssession on a remote computer and entered that possession. From within that session I use start-process to start notepad. I can confirm that notepad is running with the get-process command, and also with taskmgr in the remote computer. However, the GUI side of the process isn't showing. This is the sequence I've been using:
$server = New-PSSession -ComputerName myserver -Credential mycreds
Enter-PSSession $server
[$server]: PS C:\>Start-Process notepad -Wait -WindowStyle Maximized
The process is running, but while RDP'd to the box, notepad does not open. If I open notepad from the server, a new notepad process begins. I also tried by using the verb parameter like this:
[$server]: PS C:\>Start-Process notepad -Wait -WindowStyle Maximized -Verb Open
Same result tho... Process starts, but no notepad shows. I've tried this while remoted into the box (but issued from my local host) as well as before remoting into the server.
That is because your powershell session on the remote machine does not go to any visible desktop, but to an invisible system desktop. The receiving end of your powershell remote session is a Windows service. The process is started, but nor you nor anyone else can ever see it.
And if you think about it, since multiple users could RDP to the same machine, there is really no reason to assume a remote powershell session would end up showing on any of the users desktops. Actually, in almost all cases you wouldn't want it anyway.
psexec with the -i parameter is able to do what you want, but you have to specify which of the sessions (users) you want it to show up in.
I know this is old, but I came across it looking for the solution myself so I wanted to update it for future poor souls.
A native workaround for this problem is to use a scheduled task. That will use the active session
function Start-Process-Active
{
param
(
[System.Management.Automation.Runspaces.PSSession]$Session,
[string]$Executable,
[string]$Argument,
[string]$WorkingDirectory,
[string]$UserID
)
if (($Session -eq $null) -or ($Session.Availability -ne [System.Management.Automation.Runspaces.RunspaceAvailability]::Available))
{
$Session.Availability
throw [System.Exception] "Session is not availabile"
}
Invoke-Command -Session $Session -ArgumentList $Executable,$Argument,$WorkingDirectory,$UserID -ScriptBlock {
param($Executable, $Argument, $WorkingDirectory, $UserID)
$action = New-ScheduledTaskAction -Execute $Executable -Argument $Argument -WorkingDirectory $WorkingDirectory
$principal = New-ScheduledTaskPrincipal -userid $UserID
$task = New-ScheduledTask -Action $action -Principal $principal
$taskname = "_StartProcessActiveTask"
try
{
$registeredTask = Get-ScheduledTask $taskname -ErrorAction SilentlyContinue
}
catch
{
$registeredTask = $null
}
if ($registeredTask)
{
Unregister-ScheduledTask -InputObject $registeredTask -Confirm:$false
}
$registeredTask = Register-ScheduledTask $taskname -InputObject $task
Start-ScheduledTask -InputObject $registeredTask
Unregister-ScheduledTask -InputObject $registeredTask -Confirm:$false
}
}
When you use New-PSSession and then RDP into that same computer, you're actually using two separate and distinct user login sessions. Therefore, the Notepad.exe process you started in the PSSession isn't visible to your RDP session (except as another running process via Task Manager or get-process).
Once you've RDP'd into the server (after doing what you wrote in your post), start another Notepad instance from there. Then drop to PowerShell & run this: get-process -name notepad |select name,processid
Note that there are two instances, each in a different session.
Now open up Task Manager and look at the user sessions. Your RDP session will probably be listed as session 1.
Now quit Notepad and run get-process again. You'll see one instance, but for session 0. That's the one you created in your remote PSSession.
There are only 2 workarounds that I know of that can make this happen.
Create a task schedule as the logged in user, with no trigger and trigger it manually.
Create a service that starts the process with a duplicated token of the logged in user.
For the task schedule way I will say that new-scheduledtask is only available in Windows 8+. For windows 7 you need to connect to the Schedule Service to create the task like this (this example also starts the task at logon);
$sched = new-object -ComObject("Schedule.Service")
$sched.connect()
$schedpath = $sched.getFolder("\")
$domain = "myDomain"
$user="myuser"
$domuser= "${domain}\${user}"
$task = $sched.newTask(0) # 0 - reserved for future use
$task.RegistrationInfo.Description = "Start My Application"
$task.Settings.DisallowStartIfOnBatteries=$false
$task.Settings.ExecutionTimeLimit="PT0S" # there's no limit
$task.settings.priority=0 # highest
$task.Settings.IdleSettings.StopOnIdleEnd=$false
$task.settings.StopIfGoingOnBatteries=$false
$trigger=$task.Triggers.create(9) # 9 - at logon
$trigger.userid="$domuser" # at logon
$action=$task.actions.create(0) # 0 - execute a command
$action.path="C:\windows\system32\cmd.exe"
$action.arguments='/c "c:\program files\vendor\product\executable.exe"'
$action.WorkingDirectory="c:\program files\vendor\product\"
$task.principal.Id="Author"
$task.principal.UserId="$domuser"
$task.principal.LogonType=3 # 3 - run only when logged on
$task.principal.runlevel=1 # with elevated privs
# 6 - TASK_CREATE_OR_UPDATE
$schedpath.RegisterTaskDefinition("MyApplication",$viztask,6,$null,$null,$null)
Creating a service is way more complicated, so I'll only outline the calls needed to make it happen. The easy way is to use the invoke-asservice script on powershell gallery: https://www.powershellgallery.com/packages/InvokeAsSystem/1.0.0.0/Content/Invoke-AsService.ps1
Use WTSOpenServer and WTSEnumerateSessions to get the list of sessions on the machine. You also need to use WTSQuerySessionInformation on each session to get additional information like username. Remember to free your resources using WTSFreeMemory and WTSCloseServer You'll end up with some data which looks like this (this is from the qwinsta command);
SESSIONNAME USERNAME ID STATE
services 0 Disc
>rdp-tcp#2 mheath 1 Active
console 2 Conn
rdp-tcp 65536 Listen
Here's an SO post about getting this data; How do you retrieve a list of logged-in/connected users in .NET?
This is where you implement your logic to determine which session to target, do you want to display it on the Active desktop regardless of how it's being presented, over RDP or on the local console? And also what will you do if there is no one logged on? (I've setup auto logon and call a lock desktop command at logon so that a logged in user is available.)
You need to find the process id of a process that is running on the desktop as that user. You could go for explorer, but your machine might be Server Core, which explorer isn't running by default. Also not a good idea to target winlogon because it's running as system, or dwm as it's running as an unprivileged user.
The following commands need to run in a service as they require privileges that only system services have. Use OpenProcess to get the process handle, use OpenProcessToken to get the security token of the process, duplicate the token using DuplicateTokenEx then call ``CreateProcessAsUser``` and finally Close your handles.
The second half of this code is implemented in invoke-asservice powershell script.
You can also use the sysinternals tool psexec, I didn't list it as a 3rd way because it just automates the process of creating a service.