How can my script call functions in another scripts in remote session? - powershell

Say I have two scripts:
Script 1:
helper.ps1 with contents:
#helper.ps1
Function foo
{
# do something
}
Script 2:
worker.ps1 with contents:
#worker.ps1
. 'c:\helper.ps1' # This is the correct file location, verified
Write-Output "Starting foo..."
foo
Write-Output "Done"
These two files are already uploaded to the remote server, and I try to run these using remote session with Invoke-Command:
Invoke-Command -ScriptBlock {
param($script)
& $script
} -Args 'worker.ps1'
It turns out most part of worker.ps1 is working correctly, in the above example, we will be able to get the output of line 1 and line 3.
However, it cannot run the function foo, with exception says that it is not function/script/anything, which basically means the helper.ps1 is not loaded correctly:
The term 'foo' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included.
The question is, is this the expected behavior? Can we not load other script in one script using remote session control, even when both files are uploaded and existed in the remote server?
Invoke command below:
Invoke-Command -ScriptBlock {param($script) & $script} -Args 'worker.ps1' .
Exception: The term 'foo' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included. I think Invoke-Command does what it does, since I got other lines executed without problem

I messed with this a bit and found the following to work:
worker.ps1 file:
#worker.ps1
. "c:\helper.ps1"
Write-Output "Starting foo..."
foo
Write-Output "Done"
helper.ps1 file:
#helper.ps1
Write-Host "Loading foo function..."
Function foo
{
# do something
Write-Host "The foo is alive!"
}
Write-Host "Foo loaded"
Command on remote system:
Invoke-Command -ScriptBlock {param($script) & $script} `
-ArgumentList 'c:\Worker.ps1' -ComputerName Machine
Output:
Loading foo function...
Foo loaded
Starting foo...
The foo is alive!
Done
So it may just be an issue with the single quotes instead of double quotes around the filename. I was able to execute the Invoke-Command block from two different systems to this machine and had the same results on both. (All my systems are running PS v4 so you may see different results on PS v3.)

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

Invoke remote .exe on server with argument list

I'm quite new to PowerShell and I'm hoping to ask a question about a command I'm attempting to run.
I've read and read everything I can find on this so apologies in advance if I'm asking the impossible or something dumb.
From the Windows CLI on the remote computer I can run the following command;
'c:\config-files\app.exe foo /o /last' the exe generates an output file by reading the foo files and saves it as foo.txt.
app.exe doesn't exist in within the c:\config-files, when running it
on the computer the app.exe is in the local env path within
c:\main-app.
- The above is one of the key points here which has been addressed in replies below.
I've tried adding a path to the exe but that seems to be ignored when performing the following;
path='c:\main-app\'
& Invoke-Command -ComputerName foo -ScriptBlock { & cmd.exe /c "c:\config-files\app" } -ArgumentList 'foo', '/last', '/o'
The above fails (probably obvious to some!)
If I run:
Invoke-Command foo -ScriptBlock {& cmd.exe /c "c:\main-app\app" }
The application runs in the PowerShell window, I just then seem to be unable to send Arguments to the application.
Invoke-Command -Computername foo -ScriptBlock {param ($myarg) "cmd.exe /c c:\main-app\app" $myarg } -ArgumentList 'foo', '/last', '/o'
This is the closest I think I've got, but it only reads one argument and that's being invoked from the documents and setting folder of the user attempting to execute the command and not that path of the binary.
I've tried many, many things to make this work but still don't seem to be able to get past this point, any help you can provide on this would be hugely appreciated.
Thanks in advance for your time.
You don't need cmd /c to invoke a console application (or any external program).
To access arguments passed to a script block from within the script, either use the automatic $Args array or declare parameters explicitly (as you've attempted with a single parameter).
You can use an array directly for passing its elements as individual arguments to an external program.
Invoke-Command -Computername foo -ScriptBlock {
c:\main-app\app $Args # invoke app.exe, passing arguments through
} -ArgumentList 'foo', '/last', '/o'
Additionally, you mention wanting to interpret arguments that are file paths as relative to the directory in which the application is located; the simplest solution is to use a Set-Location command first:
Invoke-Command -Computername foo -ScriptBlock {
Set-Location c:\main-app
.\app $Args
} -ArgumentList 'foo', '/last', '/o'

Invoking custom cmdlet from script block in PowerShell

I'm very new to PowerShell. I wrote a cmdlet which works fine. However, when I try and invoke it inside a job...
. .\MyCmdlet.ps1 # Dot Source
$GetProcesssJob = Start-Job -ScriptBlock {
MyCmdlet
} -Credential $specialCredentials
...I get the error that it's "not recognized as the name of a cmdlet, function, script file, or operable program". What am I doing wrong?
My problems were two-fold. As TheIncorrigible1 pointed out, I needed to put the dot-sourcing inside the ScriptBlock. However, I had tried that previously and it didn't work. I now realize that's because the credentials I was using in $specialCredentials didn't have access privileges to the file MyCmdlet.ps1!

Import functions in a script with parameters

I have a script with parameters:
param (
[Parameter(Mandatory=$true)][string]$VaultName,
[Parameter(Mandatory=$true)][string]$SecretName,
[Parameter(Mandatory=$true)][bool]$AddToMono = $false
)
...
In this script I want to include functions that I wrote in another ps1 file : common.ps1
I usually import this with
. .\common.ps1
but if I do that in the script I get:
The term '.\common.ps1' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the
path is correct and try again.
How do I import common.ps1 in this script?
Thanks!
The problem is that you are running the script from a different directory. PowerShell is looking for .\common.ps1 using the current directory, not the directory of the script. To get around this, use the built-in variable $PSScriptRoot, which contains the path of the current script. (I'm assuming you are using PowerShell v3.0 or later.)
common.ps1
function foo {
return "from foo"
}
with_params.ps1
param (
[Parameter(Mandatory=$true)][string]$VaultName,
[Parameter(Mandatory=$true)][string]$SecretName,
[Parameter(Mandatory=$true)][bool]$AddToMono = $false
)
. $PSScriptRoot\common.ps1
Write-Output "Vault name is $VaultName"
foo
I then executed this:
PS> .\some_other_folder\with_params.ps1 -VaultName myVault -SecretName secretName -AddToMono $false
and got this output:
Vault name is myVault
from foo

Get script directory in PowerShell when script is invoked with Invoke-Command

I have a set of PowerShell scripts that include a "common" script, located in the same folder, like this:
# some-script.ps1
$scriptDir = Split-Path -Parent $myinvocation.mycommand.path
. "$scriptDir\script-utils.ps1"
This is fine if the script is called directly, e.g.
.\some-script.ps1
However, if the script is called with Invoke-Command, this does not work:
Invoke-Command -ComputerName server01 -FilePath "X:\some-script.ps1"
In this case, infact, $myinvocation.mycommand contains the contents of the script, and $myinvocation.mycommand.path is null.
How can I determine the script's directory in a way that works also when the script is invoked with Invoke-Command?
NOTE
In the end, this is the solution I actually used:
Invoke-Command -ComputerName server01 `
{param($scriptArg); & X:\some-script.ps1 $scriptArg } `
-ArgumentList $something
This also allows passing parameters to the script.
I don't believe you can, from within the invoked script. From get-help invoke-command:
-FilePath
Runs the specified local script on one or more remote computers. Enter the path and file name of the script, or
pipe a script path to Invoke-Command. The script must reside on the local computer or in a directory that the
local computer can access. Use the ArgumentList parameter to specify the values of parameters in the script.
**When you use this parameter, Windows PowerShell converts the contents of the specified script file to a script
block, transmits the script block to the remote computer, and runs it on the remote computer.**
When you use invoke-command using the -filepath parameter, the script is read from the file on the local computer, converted to a script block, and that's what gets passed to the remote computer. The remote computer doesn't have any way of knowing if that script block was read from a file.
For the remote computer to know what that original file path was, you'll have to tell it. I think the easiest way to do that would be to write a function to do the invocation, and have it pass the filename to the invoked script as a parameter.
alternatively rather than using filepath.. you could pass in a scriptblock, that dotsources the script from a UNC path that all machines have access to. However each machine will need to have the appropriate executionpolicy set so that they can run that script from the UNC path
let me be clear though, that i'm not saying to run the script from the UNC path as if that does the remoting, but still using invoke-command or start-job to run a scriptblock on a remote computer. It just happens that that scriptblock will run the script from a UNC path for convenience.