PowerShell Start-Job not creating or amending files - powershell

I am new to PowerShell and this is the first time I am attempting to use a job. I am running into an issue where I have a part of a script that looks for a file, creates it if it doesn't exist and then amends the file, and when I run the script (not as a job) it executes correctly, but when I put it in a job, it doesn't amend the file.
A much simplified example of what I have is this:
Start-job -Name HostCheck -ScriptBlock {
ForEach ($Host in (Get-Content -Path .\HostFile.txt) {
Add-Content .\somefile.txt "`nWrite something on a new line for $Host"
} | Out-Null
}
# Removes job once it is finished
Get-Job -Name HostCheck | Wait-Job | Remove-Job
Now I have tried adding | Receive-Job after the | Out-Null, but that didn't seem to change anything.
I've seen people write the entire script-block to a variable and just use the variable instead, so I am curious if that is a requirement (but I wouldn't think so).
Also, this might matter, I open the script with a .bat file that escalates the PowerShell console to admin as well as setting the execution policy of the process to Bypass. Now it seems that everything that runs in that console session or is kicked off by that console session (several scripts get ran, this is just part of one of them) seems to inherit those settings, but being new with jobs, I don't know if it would also inherit those settings, or how I would force it to (if not).

I discovered the problem:
-Your current working directory is lost when starting a job so my relative path .\somefile.txt would default to C:\Users\[Username]\Documents instead of the location where the .\somefile.txt resides.
I can get around this by using an absolute path, or I think there is a way to pass arguments to a job, but if anyone knows a better way to do this, please feel free to comment.

Here's a workaround, cd to the current dir of the caller.
start-job { cd $using:pwd; pwd } | Receive-Job -wait -auto

Related

Why working script fails when running in the background?

I have such script (here simplified):
while ($true)
{
Write-Host "Looping..."
Write-Host "Looping..." 6>> trash.txt
Start-Sleep -s 1
}
when I run it directly it works, but when I run it in the background:
Start-Job { .\sleeper.ps1 }
for a second it is seen as Running but shortly after as Failed and indeed file "trash.txt" is not created at all, so even one iteration is not executed.
What is wrong here?
I think the main issue is around the $PWD and -FilePath param, but I will list some info on Write-Host too:
Start-Job should be run with the -FilePath parameter. This is because {} is a ScriptBlock object, which is by default taken by the -ScriptBlock parameter, which you do not want. Example solution to that line: Start-Job -FilePath ./script.ps1
The $PWD or present working directory is, by default, the home directory / user profile of the current user when executed in a PowerShell job (Linux: $HOME // Windows: $Home/Documents). You can test this by executing a job simply with that variable (it may be $ENV:PWD on Windows?). Either way, it is likely not the same as the directory you are executing this in. trash.txt is being made and appended to, but it is being made in a different directory than your current directory. You will want your script to explicitly include an absolute path to the file being created, or give the script parameters that allow you to input the path at execution. Here is another StackOverflow article where a user had similar struggles, with two good solutions where one uses $args in the script and another uses the -InitializationScript parameter of Start-Job to set the $PWD: PowerShell Start-Job Working Directory
Often, Write-Host is low-priority as a selected output vs. using Write-Output / Write-Verbose / Write-Warning / Write-Error. More information about this can be found here: https://www.jsnover.com/blog/2013/12/07/write-host-considered-harmful/ - though, newer versions of PowerShell have added an information stream which I believe may make Write-Host output more accessible. More information on streams can be found with help about_Redirection

Invoking long NAnt process from PowerShell form Jenkins (using Pipelines)

I've been working on wrapping up the usage of some old NAnt scripts behind a Jenkins job. The Jenkins job itself is using the pipelines feature, a groovy DSL script, one of the steps is a PowerShell block, and it calls some a function that invokes NAnt, after working out lots of parameters to be parsed in.
I did have this working at some point just fine, but something has broken at some stage. The PowerShell function is called, and it triggers NAnt, and for the nearly an hour that it takes to complete, you get the output, as it happens, showing up in Jenkins.
This was done using something like Invoke-Expression "& $NAntExe $NAntFile $Target $ParameterString" | Write-Host, where $ParameterString is all the -D:Key=Value parameters.
I believe I had added the | Write-Host as without it, you only get the output at the very end, but we wanted to be able to see the progress as it's happening.
As I said, something has changed somewhere, and we were no longer getting any output from NAnt. I eventually found that removing the | Write-Host would restore the logs, but as I expected, we now have to wait for NAnt to finish before we see any logs.
What is the 'correct' way to invoke NAnt here to get the output as I desire? I want to see the output as it happens.
I've tried various ways of invoking NAnt, with no luck. Seems I'm having to settle for either "I get all the output in one go at the end" or "no output". I suspect this is not a PowerShell issue as such, but that's based on nothing but gut feeling.
Seems I can mostly recreate the symptoms I see in Jenkins. If I invoke NAnt through a fresh PowerShell session I get the same problem, I'm running something akin the following, which as far as I can tell would be the same as how the Jenkins plugin invokes PowerShell:
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy ByPass -Command 'Invoke-FunctionThatCallsNAnt'
Within my Invoke-FunctionThatCallsNAnt, I had initially, as I said above, just directly called NAnt and got no logging. I then update my function to pipe the output to Write-Host or I can remove the -NonInteractive flag and I will get the output from NAnt in real time. However, when I go to Jenkins, this does not resolve the problem, I end up with getting no output at all.
I'm not sure why it wouldn't stream. You should be able to write the command these ways:
& $NAntExe $NAntFile $Target $ParameterString
Or with whatever the nant command is.
$env:path += ';c:\program files\nant' # add to path if needed
nant.exe $NAntFile $Target $ParameterString
If it's not in the path, and the folder doesn't have spaces, you can put the whole path to it as well.
c:\nant\nant.exe $NAntFile $Target $ParameterString
EDIT:
Here's a way to run something in a path with spaces:
C:\Program` Files\Internet` Explorer\iexplore.exe
EDIT2:
It looks like you have to unblock the nant zip after downloading it: How do I resolve configuration errors with Nant 0.91?
Or unblock all the files after the fact:
get-childitem -recurse c:\nant-92 |
get-item -stream zone.identifier -erroraction silentlycontinue |
select -expand filename | get-item | unblock-file

How do I get a Powershell process that was opened by another Powershell process?

I am running multiple PowerShell scripts at once. I would like to be able to wait on certain ones to finish before opening new scripts. Basically, I was thinking if I could find the command line option that ran it something like "powershell.exe -Path "<script dir>" that would do it.
I tried doing a Get-Process | gm to find any parameters that I could call to get that information and I didn't see any (doesn't mean they aren't there) I tried looking through Task Manager to see if I could view something through the gui that I could link to but that didn't help either.
I hope I can get something like
Start-Process -FilePath ".\<script>.ps1" -ArgumentList "<args>"
do
{
sleep 10
}
until ((Get-Process -ProcessName "PowerShell" | where "<paramater>" -EQ ".\<script>")
I need to wait until that process is done but I don't want to put a wait at the end of the Start-Process because after that Start-Process kicks off I need some other items to go to while my .\ is running. I just need it to wait before another section of script kicks off.
Have a look at the "Job" cmdlets https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_jobs?view=powershell-6
And the $PID automatic variable, this will give the process ID of the current PowerShell session.

Combining powershell & cmd to create a batch file

I am not a coder but I have some basic ability to mash some scripts together and usually get something that works for me, but this is way beyond my comprehension. I am trying to combine 3 things into a batch file. It's essentially an all in one solution to turn Windows 10 privacy settings off and to remove some background apps. I can run the commands just fine in powershell, but I wanted to automate the entire thing through one batch script:
Keep the windows 10 privacy settings script up to date. This is done through the following command in powershell:
(New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/hahndorf/Set-Privacy/master/Set-Privacy.ps1') | out-file .\Set-Privacy.ps1 -force
Just need to make sure that the destination folder for download is set first (I have no idea how to do this through a batchfile/script). The command comes from the script that someone wrote on github located here
Run the powershell script to turn privacy settings off, on the highest setting. This is done through the following command, again from the link above:
.\Set-Privacy.ps1 -Strong -admin
Remove background apps, for example, removing "3D Builder":
To uninstall 3D Builder:
get-appxpackage *3dbuilder* | remove-appxpackage
Details on this found here
Any help with any of this is greatly appreciated.
Try this, of course PowerShell is not a batch file tool per se, though you can use it to call a batch (.bat,.cmd.vbs, etc.) file.
Though a script is a script (batch or otherwise), taxonomy is a whole different conversation.
So, what you want is a function you can call as needed.
Function Set-Windows10PrivacySettings
{
[CmdletBinding()]
[Alias()]
Param
(
[string]$DownloadPath = "$env:USERPROFILE\Downloads"
)
# Download the Privacy Script
(New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/hahndorf/Set-Privacy/master/Set-Privacy.ps1') |
out-file "$env:USERPROFILE\Downloads\Set-Privacy.ps1"
# Temporarialy naviagte to the target directory adn run the script
Push-Location -Path $DownloadPath
Start-Process .\Set-Privacy.ps1 -Strong -admin
# Remove AppX packages
$AppPackageList = (Get-AppxPackage).Name |
Sort-Object |
Out-GridView -Title 'Select one or more AppX to remove. Press CRTL+LeftMouseClick to multi-select' -PassThru
# Remove the selected AppX without prompt to confirm
ForEach($AppPackage in $AppPackageList)
{
"Removing selected AppX $AppPackage"
# Remove-AppxPackage -Package $_ -Confirm:$false -WhatIf
}
# Retrun to the original directory
Pop-Location
}
# Call the function in PowerShell
Set-Windows10PrivacySettings
Just remove the comment marker '#' and the 'WhatIf' switch to actually allow things to happen.

Call a PowerShell script in a new, clean PowerShell instance (from within another script)

I have many scripts. After making changes, I like to run them all to see if I broke anything. I wrote a script to loop through each, running it on fresh data.
Inside my loop I'm currently running powershell.exe -command <path to script>. I don't know if that's the best way to do this, or if the two instances are totally separate from each other.
What's the preferred way to run a script in a clean instance of PowerShell? Or should I be saying "session"?
Using powershell.exe seems to be a good approach but with its pros and cons, of course.
Pros:
Each script is invoked in a separate clean session.
Even crashes do not stop the whole testing process.
Cons:
Invoking powershell.exe is somewhat slow.
Testing depends on exit codes but 0 does not always mean success.
None of the cons is mentioned is a question as a potential problem.
The demo script is below. It has been tested with PS v2 and v3. Script names
may include special characters like spaces, apostrophes, brackets, backticks,
dollars. One mentioned in comments requirement is ability to get script paths
in their code. With the proposed approach scripts can get their own path as
$MyInvocation.MyCommand.Path
# make a script list, use the full paths or explicit relative paths
$scripts = #(
'.\test1.ps1' # good name
'.\test 2.ps1' # with a space
".\test '3'.ps1" # with apostrophes
".\test [4].ps1" # with brackets
'.\test `5`.ps1' # with backticks
'.\test $6.ps1' # with a dollar
'.\test ''3'' [4] `5` $6.ps1' # all specials
)
# process each script in the list
foreach($script in $scripts) {
# make a command; mind &, ' around the path, and escaping '
$command = "& '" + $script.Replace("'", "''") + "'"
# invoke the command, i.e. the script in a separate process
powershell.exe -command $command
# check for the exit code (assuming 0 is for success)
if ($LastExitCode) {
# in this demo just write a warning
Write-Warning "Script $script failed."
}
else {
Write-Host "Script $script succeeded."
}
}
If you're on PowerShell 2.0 or higher, you can use jobs to do this. Each job runs in a separate PowerShell process e.g.:
$scripts = ".\script1.ps1", ".\script2.ps1"
$jobs = #()
foreach ($script in $scripts)
{
$jobs += Start-Job -FilePath $script
}
Wait-Job $jobs
foreach ($job in $jobs)
{
"*" * 60
"Status of '$($job.Command)' is $($job.State)"
"Script output:"
Receive-Job $job
}
Also, check out the PowerShell Community Extensions. It has a Test-Script command that can detect syntax errors in a script file. Of course, it won't catch runtime errors.
One tip for PowerShell V3 users: we (the PowerShell team) added a new API on the Runspace class called ResetRunspace(). This API resets the global variable table back to the initial state for that runspace (as well as cleaning up a few other things). What it doesn't do is clean out function definitions, types and format files or unload modules. This allows the API to be much faster. Also note that the Runspace has to have been created using an InitialSessionState object, not a RunspaceConfiguration instance. ResetRunspace() was added as part of the Workflow feature in V3 to support parallel execution efficiently in a script.
The two instances are totally separate, because they are two different processes. Generally, it is not the most efficient way to start a Powershell process for every script run. Depending on the number of scripts and how often you re-run them, it may be affecting your overall performance. If it's not, I would leave everything AS IS.
Another option would be to run in the same runspace (this is a correct word for it), but clean everything up every time. See this answer for a way to do it. Or use below extract:
$sysvars = get-variable | select -Expand name
function remove-uservars {
get-variable |
where {$sysvars -notcontains $_.name} |
remove-variable
}