Powershell - Check for updates to script prior to running - powershell

I was wondering if anyone knew of a way to have a powershell script check for updates to itself prior to running.
I have a script that I am going to be dispatching out to multiple computers, and don't want to have to redeploy it to each computer each time a change in the script is made. I would like to have it check a certain location to see if there is a newer version of itself (and update itself if needed).
I can't seem to think of a way to do it. Please let me know if anyone can help out. Thanks.

Well, one way maybe to create a simple batch file that runs your actual script and the first line in that batch file may be to check for existence of a ps1 in your update folder. If there is one, it can copy it down first, and then start your powershell script
Eg. whenever there is an update, you put your 'Mypowershellscript.ps1' script in c:\temp\update\ folder
and let's assume your script will be running from
c:\temp\myscriptfolder\
then you can create batch file like this
if NOT exist C:\temp\update\mypowershelscript.ps1 goto :end
copy /Y c:\temp\update\MyPowerShellScript.ps1 c:\temp\MyScriptFolder\
:END
%systemroot%\System32\WindowsPowerShell\v1.0\powershell.exe -nologo -noprofile -file "c:\temp\myscriptfolder\mypowershellscript.ps1"

Here's a function I put together. Pass it the path of the file that might hold a newer release. This will update itself and then re-run with any arguments handed to the original script. Do this early in the process, other function results will be lost. I typically check the network is up and I can see the share holding the newer file, then run this:
function Update-Myself
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true,
Position = 0)]
[string]$SourcePath
)
#Check that the file we're comparing against exists
if (Test-Path $SourcePath)
{
#The path of THIS script
$CurrentScript = $MyInvocation.ScriptName
if (!($SourcePath -eq $CurrentScript ))
{
if ($(Get-Item $SourcePath).LastWriteTimeUtc -gt $(Get-Item $CurrentScript ).LastWriteTimeUtc)
{
write-host "Updating..."
Copy-Item $SourcePath $CurrentScript
#If the script was updated, run it with orginal parameters
&$CurrentScript $script:args
exit
}
}
}
write-host "No update required"
}
Update-Myself "\\path\to\newest\release\of\file.ps1"

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

Test-Path in local directory

Consider this simple path validation:
if ((Test-Path $_) -and ($_ -like "*.msi")) {
$true
}
else {
Throw "Specify correct path to installer."
}
When passing a path e.g. "C:\Scripts\installer.msi" the validation works as expected.
But when executing in same directory as the installer, passing this argument as the path: .\Installer.msi
Validation is still True but breaks the installer.
How can I fix this?
Assuming that you're using msiexec.exe to run this installer, you have to pass the full path to that msi file in order for it to work.
So you're probably calling msiexec like this:
msiexec /i $_
when really you would have to do this:
msiexec /i "$((Get-Item $_).FullName)"
You might also want to look into the MSI PowerShell module. It makes working with msi files a bit nicer within PowerShell.

How can I update the current script before running it?

We have some PowerShell scripts living on network share A, and the latest versions of those scripts living on read-only network share B. The read-only share is the output of our build system, and it has the build number in the path. Partly out of habit, partly because the scripts must create files on disk, but mostly because the paths are predictable, we copy PowerShell scripts to network share A before executing them. The trouble is that we don't always copy the scripts to network share A, so occasionally those scripts are out of date.
I'm trying to create a script that will update the PowerShell scripts on network share A (by copying the latest versions from share B), and then execute those latest scripts. Right now, my idea is to have a tiny script that grabs the latest script from share B, copies it to share A, and then executes that script on share A.
Is it possible to have a script update itself? I.e., instead of having two scripts, can I have one script (that lives on share A) that copies a newer version itself from share B to share A, then restarts execution of itself? (I would put in some logic about file-creation date so that on first execution, it would update itself, and on second execution it would run the actual meat of the script.)
Yes, you can update the script you're running, then execute it again. (Just make sure to exit the first script after updating.) Here's some sample code I created:
Write-Host "Starting script"
if ($(Get-Item G:\selfUpdater2.ps1).CreationTimeUtc -gt $(Get-Item G:\selfUpdater.ps1).CreationTimeUtc) {
Copy-Item G:\selfUpdater2.ps1 G:\selfUpdater.ps1
$(Get-Item G:\selfUpdater.ps1).CreationTimeUtc = [DateTime]::UtcNow
&G:\selfUpdater.ps1
exit
}
Write-Host "Continuing original script; will not get here if we updated."
Note that, if you have parameters to pass around, you'll have to pass them to the target script. Since your updated script may well have more or fewer parameters than your current script (some bound, some unbound by the current script), you'll need to iterate through both $script:MyInvocation.BoundParameters and $script:MyInvocation.UnboundArguments to pick up all of them and pass them on.
(Personally, I've had more luck with random-parameter-passing using Invoke-Expression ".\scriptName.ps1 $stringOfArguments" than with &.\scriptName.ps1 $arguments, but your mileage may vary - or you may know more PowerShell than I do. If you use Invoke-Expression, then be sure to re-add quotes around any parameters that have spaces in them.)
There's one drawback: If a mandatory script parameter is removed in a future version of the script, then you need to run the script at least once with the no-longer-mandatory parameter before it will update itself allow you to drop the parameter.
Here's a function I put together. Pass it the path of the file that might hold a newer release. This will update itself and then re-run with any arguments handed to he original script.
function Update-Myself
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true,
Position = 0)]
[string]$SourcePath
)
#check that the destination file exists
if (Test-Path $SourcePath)
{
#The path of THIS script
$CurrentScript = $MyInvocation.ScriptName
if (!($SourcePath -eq $CurrentScript ))
{
if ($(Get-Item $SourcePath).LastWriteTimeUtc -gt $(Get-Item $CurrentScript ).LastWriteTimeUtc)
{
write-host "Updating..."
Copy-Item $SourcePath $CurrentScript
#If the script was updated, run it with orginal parameters
&$CurrentScript $script:args
exit
}
}
}
write-host "No update required"
}
Update-Myself \\path\to\newest\release\of\file.ps1

Powershell_ise doesn't refresh modification done outside

How to refresh Powershell_ise for contents modified outside the IDE.
Most of the time i would have opened both Powershell_ise and notepad++
If i does changes in Powershell_ise , notepad++ asks for reload but if i modify in notepad++ there is no way to refresh in Powershell_ise.
Whether any way to refresh the content or am i overlooking any feature which provides this?
This post is old, but I figured I'd post this as google brought me here with the same issue.
I eventually just wrote this little function which doesn't do exactly what the OP wanted, but maybe other googlers will find it useful:
function Build {
#Reload file
$CurrentFile = $psise.CurrentFile
$FilePath = $CurrentFile.FullPath
$PsISE.CurrentPowerShellTab.Files.remove($CurrentFile)
$PsISE.CurrentPowerShellTab.Files.add($FilePath)
iex $PsISE.CurrentPowerShellTab.Files.Editor.Text
}
$psISE.CurrentPowerShellTab.AddOnsMenu.SubMenus.Clear()
$psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Reload file and run",{Build},'f4')
Its not perfect, but its good enough for me for now. All is does is create a key binding that closes,reopens, and then executes the current file. Its a bit jarring though because when you run it you'll lose your current cursor position when the file is closed and reopened. I'm sure you could store the column and line position of the cursor and restore it when reloading, but I'm too lazy to bother with that for the time being.
Edit: I accidentally posted an older non-working version of my code. Updated with working version.
Here is a different spin on red888's script:
function Reload {
$CurrentFile = $psise.CurrentFile
$FilePath = $CurrentFile.FullPath
$lineNum = $psise.CurrentFile.Editor.CaretLine
$colNum = $psise.CurrentFile.Editor.CaretColumn
$PsISE.CurrentPowerShellTab.Files.remove($CurrentFile) > $null
$newFile = $PsISE.CurrentPowerShellTab.Files.add($FilePath)
$newfile.Editor.SetCaretPosition($lineNum,$colNum)
}
$psISE.CurrentPowerShellTab.AddOnsMenu.SubMenus.Clear()
$psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Reload File",{Reload},'f4') > $null
It restores the position of the caret after reload. I removed the line
iex $PsISE.CurrentPowerShellTab.Files.Editor.Text
As I didn't need it and its also not the same as running the script (and so results in strange behavior of statements like $script:MyInvocation.MyCommand.Path).
Incidentally, if you put this code in your ISE profile it will automatically run when you first load the ISE. The ISE profile is just a powershell script whose location is given by the $profile variable.
Here are some commands that create the profile if it doesn't exist, and then opens it. Run it from inside the ISE:
if (!(Test-Path (Split-Path $profile))) { mkdir (Split-Path $profile) } ;
if (!(Test-Path $profile)) { New-Item $profile -ItemType file } ;
notepad $profile
PowerShell ISE does not support refreshing the changed files automatically. It is not there even in ISE v3.
There is connect suggestion on this topic: https://connect.microsoft.com/PowerShell/feedback/details/711915/open-ise-files-should-update-when-edited-externally
However, this can be done using PowerShell ISE Object model and PowerShell eventing. Explore $psise.CurrentFile and $psise.CurrentPowerShellTab.Files collection. This must give you enough information to write your own simple addon.

PowerShell: Run command from script's directory

I have a PowerShell script that does some stuff using the script’s current directory. So when inside that directory, running .\script.ps1 works correctly.
Now I want to call that script from a different directory without changing the referencing directory of the script. So I want to call ..\..\dir\script.ps1 and still want that script to behave as it was called from inside its directory.
How do I do that, or how do I modify a script so it can run from any directory?
Do you mean you want the script's own path so you can reference a file next to the script? Try this:
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
Write-host "My directory is $dir"
You can get a lot of info from $MyInvocation and its properties.
If you want to reference a file in the current working directory, you can use Resolve-Path or Get-ChildItem:
$filepath = Resolve-Path "somefile.txt"
EDIT (based on comment from OP):
# temporarily change to the correct folder
Push-Location $dir
# do stuff, call ant, etc
# now back to previous directory
Pop-Location
There's probably other ways of achieving something similar using Invoke-Command as well.
There are answers with big number of votes, but when I read your question, I thought you wanted to know the directory where the script is, not that where the script is running. You can get the information with powershell's auto variables
$PSScriptRoot # the directory where the script exists, not the
# target directory the script is running in
$PSCommandPath # the full path of the script
For example, I have a $profile script that finds a Visual Studio solution file and starts it. I wanted to store the full path, once a solution file is started. But I wanted to save the file where the original script exists. So I used $PsScriptRoot.
If you're calling native apps, you need to worry about [Environment]::CurrentDirectory not about PowerShell's $PWD current directory. For various reasons, PowerShell does not set the process' current working directory when you Set-Location or Push-Location, so you need to make sure you do so if you're running applications (or cmdlets) that expect it to be set.
In a script, you can do this:
$CWD = [Environment]::CurrentDirectory
Push-Location $MyInvocation.MyCommand.Path
[Environment]::CurrentDirectory = $PWD
## Your script code calling a native executable
Pop-Location
# Consider whether you really want to set it back:
# What if another runspace has set it in-between calls?
[Environment]::CurrentDirectory = $CWD
There's no foolproof alternative to this. Many of us put a line in our prompt function to set [Environment]::CurrentDirectory ... but that doesn't help you when you're changing the location within a script.
Two notes about the reason why this is not set by PowerShell automatically:
PowerShell can be multi-threaded. You can have multiple Runspaces (see RunspacePool, and the PSThreadJob module) running simultaneously withinin a single process. Each runspace has it's own $PWD present working directory, but there's only one process, and only one Environment.
Even when you're single-threaded, $PWD isn't always a legal CurrentDirectory (you might CD into the registry provider for instance).
If you want to put it into your prompt (which would only run in the main runspace, single-threaded), you need to use:
[Environment]::CurrentDirectory = Get-Location -PSProvider FileSystem
This would work fine.
Push-Location $PSScriptRoot
Write-Host CurrentDirectory $CurDir
I often used the following code to import a module which sit under the same directory as the running script. It will first get the directory from which powershell is running
$currentPath=Split-Path ((Get-Variable
MyInvocation -Scope
0).Value).MyCommand.Path
import-module "$currentPath\sqlps.ps1"
I made a one-liner out of #JohnL's solution:
$MyInvocation.MyCommand.Path | Split-Path | Push-Location
Well I was looking for solution for this for a while, without any scripts just from CLI. This is how I do it xD:
Navigate to folder from which you want to run script (important thing is that you have tab completions)
..\..\dir
Now surround location with double quotes, and inside them add cd, so we could invoke another instance of powershell.
"cd ..\..\dir"
Add another command to run script separated by ;, with is a command separator in powershell
"cd ..\..\dir\; script.ps1"
Finally Run it with another instance of powershell
start powershell "cd..\..\dir\; script.ps1"
This will open new powershell window, go to ..\..\dir, run script.ps1 and close window.
Note that ";" just separates commands, like you typed them one by one, if first fails second will run and next after, and next after... If you wanna keep new powershell window open you add -noexit in passed command . Note that I first navigate to desired folder so I could use tab completions (you couldn't in double quotes).
start powershell "-noexit cd..\..\dir\; script.ps1"
Use double quotes "" so you could pass directories with spaces in names e.g.,
start powershell "-noexit cd '..\..\my dir'; script.ps1"