How does powershell handle transitive / nested module imports? - powershell

I always thought that doing Import-Module in my current powershell console will make the module available throughout the console.
Recently however I have learned that when doing Import-Module from within a module, things are getting complicated.
Compare:
Windows PowerShell Session State
...
Module Session State
Module session states are created whenever the module or one of its nested modules is imported into the session. When a module exports an element such as a cmdlet, function, or script, a reference to that element is added to the global session state of the session. However, when the element is run, it is executed within the session state of the module.
What I can observe, is:
# This is mod1.psm1
Import-Module -Force mod2.psm1
# end mod1.psm1
-
PS> Import-Module -Force mod1.psm1
... This will make the functions of mod2.psm1 available via mod1, when checked with Get-Module
PS> Import-Module -Force mod2.psm1
... This will make the functions of mod2 available via mod2.
I don't even fully understand the behavior here, but now comes the interesting part:
# This is mod1.psm1
function fn_load_in_mod1 {
Import-Module -Force mod2.psm1
# mod2 stuff can be used here
}
# end mod1.psm1
PS> Import-Module -Force mod1.psm1
PS> # But here, no mod2 stuff is available anymore!
I learned that to make the mod2 stuff available to my console, I need to:
# This is mod1.psm1
function fn_load_in_mod1 {
Import-Module -Force mod2.psm1 -Global
# mod2 stuff can be used here, and with the -Global switch also "outside"
}
# end mod1.psm1
But I can't make head nor tail of that session state stuff.
How am I supposed to do an Import-Module from within a module "correctly"?

Related

Why do I have to open a new powershell window to get the changes to my psm1 files? [duplicate]

I created a custom powershell module in the
C:\Program Files\WindowsPowerShell\Modules\PennoniAppManagement directory. Whenever I make changes to a function in the module, then import the module into a script, the updated code won't take effect. Any solutions?
Make sure you remove the already-loaded version of the module from the session before re-importing it:
Remove-Module PennoniAppManagement -Force
Import-Module PennoniAppManagement
Normally, Import-Module-Force - by itself - is enough to force reloading of an updated module into the current session.
Import-Module -Force implicitly performs Remove-Module before reloading the module (if the module isn't currently loaded, -Force just loads the module normally).
Also note that force-reloading a module is not an option if you're loading it via a using module statement (at least as of PowerShell 7.1.2). Notably the using module method of importing is required if a module exports custom class definitions that the caller should see - see this answer for details.
Mathias' two-step approach - Remove-Module -Force, followed by Import-Module - is apparently needed in some cases, and seems to be required in yours.
It would be good to understand when the two-step approach is needed. Mathias thinks it is related to cached versions of custom class definitions (used module-internally) lingering instead of getting reloaded and redefined when Import-Module -Force is called. That is, while the module overall may get reloaded, it may be operating on stale classes. At least in the simple scenario below I was not able to reproduce this problem, neither in Windows PowerShell 5.1, nor in PowerShell (Core) 7.2.1, but there may be scenarios where the problem does surface.
The Remove-Module documentation describes the -Force parameter solely as relating to the - rarely used - .AccessMode property available on a loaded module's module-information object (you can inspect it with (Get-Module ...).AccessMode). The default value is ReadWrite, which allows unloading (removal) of the module anytime. If the property value is ReadOnly, Remove-Module -Force is needed to unload; if it is Constant, the module cannot be removed from the session at all, once loaded - at least not with Remove-Module.
Notably, the implicit unloading that happens with Import-Module -Force is not subject to these restrictions and implicitly unloads a module even if its .AccessMode is Constant (as of PowerShell 7.1.2; I am unclear on whether that is by design).
Test code involving reloading a module with a modified class definition, to see if Import-Module -Force is enough:
# Create a template for the content of a sample script module.
# Note: The doubled { and } are needed for use of the string with
# with the -f operator later.
$moduleContent = #'
class MyClass {{
[string] $Foo{0}
}}
function Get-Foo {{
# Print the property names of custom class [MyClass]
[MyClass]::new().psobject.Properties.Name
}}
'#
# Create the module with property name .Foo1 in the [MyClass] class.
$moduleContent -f 1 > .\Foo.psm1
# Import the module and call Get-Foo to echo the property name.
Import-Module .\Foo.psm1; Get-Foo
# Now update the module on disk by changing the property name
# to .Foo2
$moduleContent -f 2 > .\Foo.psm1
# Force-import (reload) the module and
# see if the property name changed.
Import-Module -Force .\Foo.psm1; Get-Foo
# Clean up.
Remove-Item .\Foo.psm1
In both Windows PowerShell (whose latest and last version is v5.1) and PowerShell (Core) 7.2.1 (current as of this writing), the above yields, as expected:
Foo1 # Original import.
Foo2 # After modifying the class and force-reloading

Hide Import-Module output from TFS Release logs

I am using TFS 2015 Release. I have a very simple script: it imports module and runs another script. The problem is I see Import-Module logs in TFS Release log and I cannot hide them. Example:
# ScriptRunner.ps1
Import-Module MyModule -DisableNameChecking -Global
& ".\script.ps1"
When I run this locally by using below command, I don't see any output from Import-Module command:
powershell "path\ScriptRunner.ps1"
In TFS Release I have "PowerShell on Target Machines" task, and I run the same script ScriptRunner.ps1. And this is what I see in the log:
Deployment started on target machine...
Loading module from path 'C:\Program Files\WindowsPowerShell\Modules\MyModule\MyModule.psm1'
Exporting function 'My-Function-1'.
...
Exporting function 'My-Function-10'.
The 'My-Function-1' command in the MyModule module was imported, but because its name does not include an approved verb, it might be difficult to find. For a list of approved verbs, type Get-Verb.
The command name 'My-Function-1' from the module 'MyModule' contains one or more of the following restricted characters: # , ( ) { } [ ] & - / \ $ ^ ; : " ' < > | ? # ` * % + = ~
Importing function 'My-Function-1'.
...
How is this getting logged? I specify DisableNameChecking flag. I tried to hide this messages by changing Import-Module line in my script, but this doesn't help:
[void](Import-Module MyModule -DisableNameChecking -Global)
Import-Module MyModule -DisableNameChecking -Global | Out-Null
Import-Module MyModule -DisableNameChecking -Global -WarningAction SilentlyContinue
You can turn of $ErrorActionPreference flag to Silentlycontinue. That should fix the issue.
Please be aware that when you add this line to the script the whole script change the Error Action Preference.
$ErrorActionPreference = "Silentlycontinue"

Import PowerShell module after install

I'm working on automating an AppFabric installation using PowerShell, and I've run into a problem where the script is calling the installer, waiting for it to complete, but I am unable to import the installed modules after from the same context. i.e:
Start-Process "C:\provision\WindowsServerAppFabricSetup_x64.exe" -ArgumentList "/i /GAC" -Wait
Import-Module DistributedCacheConfiguration
# ...do configuration things...
Which errors: The specified module 'DistributedCacheConfiguration' was not loaded because no valid module file was found in any module directory.
If you close and re-open PowerShell, the script runs fine. Adding a Start-Sleep 60 between the installer and the configuration didn't help, so I tried calling it as though powershell were restarting:
C:\WINDOWS\system32\windowspowershell\v1.0\powershell.exe C:\provision\appfabric_config.ps1
The same errors were thrown. How do I get PowerShell to recognize the newly installed modules?
PowerShell looks for modules in subdirectories of the directories listed in the PSModulePath environment variable. Environment variables are read from the registry key HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment when the session is initialized.
If the installer places the new module in a directory that's not already in PSModulePath and then adds that directory to the environment variable, it's modifying the environment variable in the registry, not in the current PowerShell console session's environment, so only PowerShell sessions started after the installation will have the updated PSModulePath.
You can manually update the value from the registry by adding the following line after the installation and before attempting to import the module:
$env:PSModulePath = (Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Session Manager\Environment' -Name PSModulePath).PSModulePath
Note that although it may appear redundant, the reason you need
(Get-ItemProperty -Path [...] -Name PSModulePath).PSModulePath
rather than just
Get-ItemProperty -Path [...] -Name PSModulePath
is that Get-ItemProperty doesn't return the data of the named registry value, it returns a PSCustomObject that contains information about the registry value, and the data is in a property of that PSCustomObject that has the name of the registry value (i.e. PSModulePath in this case). If you prefer, you could also do it this way:
$env:PSModulePath = Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Session Manager\Environment' -Name PSModulePath | select -ExpandProperty PSModulePath
(There's no practical difference, it's six or a half dozen.)
You can use the .NET System.Environment library to access your environment variables. Your new module is most likely added to your "User" environment variable. You can also experiment with specifying "Machine" and "Process" targets. See Environment.GetEnvironmentVariable for more info. Here is a good article on modifying your paths, including adding to and removing entries. It can be easily adapted to the PSModulePath environment variable.
This example adds the PSModulePath environment variable for the User to the end of the PSModulePath environment variable in your session. It will result in some duplicate entries, but should work just fine.
$env:PSModulePath = $env:PSModulePath+';'+[System.Environment]::GetEnvironmentVariable("PSModulePath","User")
Your code would now look like this:
Start-Process "C:\provision\WindowsServerAppFabricSetup_x64.exe" -ArgumentList "/i /GAC" -Wait
$env:PSModulePath = $env:PSModulePath+';'+[System.Environment]::GetEnvironmentVariable("PSModulePath","User")
Import-Module DistributedCacheConfiguration
# ...do configuration things...

Powershell implicit remoting called from another module file

We have a number of Powershell scripts that call "modules" with a load of reusable functions in. One function we have in a module sets up implicit remoting to our main DC and then imports the active directory module to the local session. I know this works as I have pasted the function into Powershell and it works. But when a script loads the module that contains the function it imports correctly but the cmdlets are not useable from the main script. I imagine this is something to do with scoping but I just can't see how to get this to work. For reference we cannout change the idea of calling modules with reusable functions in as there to many scripts that rely on it and it would be a big job to change it.
--Added for clarification from a comment below
I have a script, lets call it Script1, this script does some stuff but also loads some additional functions from other script modules using the following code
$Global:ICTSGVModuleLocation="\\server1\Scripts\Support Files\PS Modules\Global Modules"
get-childitem $Script:ICTSGVModuleLocation | foreach-object $_.name {Import-Module "$Global:ICTSGVModuleLocation\$_" -Global}
In one of the other script modules (lets call it script2) is some code to create an implicit remoting session and import an active directory module, code below
Function Global:ICTSGF.Adconnecter
{Try {$Script:connect = new-pssession -ComputerName "$Global:ICTSGVAdConnectorServer";
invoke-command -session $script:connect {import-module activedirectory -prefix cardiff};
Import-Module (Import-PSSession -session $Script:connect -module ActiveDirectory - AllowClobber) -Global}
Catch {ICTSGF.ScriptOutput "E2" "A4" "Loading Active Directory Module Failed"}
Finally {}}
In the main script (script1) i load the function by calling Global:ICTSGF.Adconnecter. This then loads the banner saying loading active Directory module and it appears to work. However when i attempt to run a command from the module imported it doesnt work
get-cardiffaduser $user
Thanks

How can I use a PowerShell module in a script without leaving the module loaded in the user's session?

I have a script I wish to use interactively from the PowerShell prompt. The script needs to use a local script module.
I cannot see how to import/use the module such that it's not left loaded in the current session.
Example
A module (MyModule.psm1)...
function Test-Method
{
write-host "Test-Method invoked"
}
... and a script (script.ps1)
Import-Module .\MyModule
Test-Method
Now running the script at the PowerShell prompt ...
PS C:\temp> Get-Module | % {$_.Name}
Microsoft.PowerShell.Management
Microsoft.PowerShell.Utility
PS C:\temp> .\script.ps1
Test-Method invoked
PS C:\temp> Get-Module | % {$_.Name}
Microsoft.PowerShell.Management
Microsoft.PowerShell.Utility
MyModule
How can my script import and use MyModule.psm1 without it being left loaded in the caller's current session? Bearing in mind that the call may have already imported the module and would not want it unloaded by the script (so simply removing the module at the completion of the script is not really good enough).
I've considered dot-sourcing the module rather than importing it, but I want the module for the reasons covered in PowerShell Import-Module vs Dot Sourcing
It sounds like you already described in pseudo-code what you wanted. Here it is in actual code:
$checkCmds = Get-Commands -Module MyModule
Import-Module MyModule
# Do stuff here . . .
# unload only if we loaded it
if ($checkCmds -eq $null) { Remove-Module MyModule }
As far as I can tell, you don't get that automatic cleanup behavior from a "script" importing a module. OTOH if you import a module from within another module, when the parent module is removed then any modules it imported will be removed if there are no other modules using them (or unless ipmo -global was specified).
This builds on the previous answer and uses the following property.
If you import a module from within another module, when the parent module is removed then any modules it imported will be removed
You can exploit several techniques to create a wrapper:
Importing a module from a module
Anonymous modules
Call operator with the context of a module
Set script.ps1 to
& (New-Module {
function Invoke-Function {
Import-Module .\MyModule
Test-Method
}
}) { Invoke-Function }
If you run script.ps1 and then (Get-Module).Name then MyModule will not be listed in the output.
Note: In this example Invoke-Function is just another scope, and can be omitted, letting the New-Module just run when defined. In one line:
& (New-Module { Import-Module .\MyModule; Test-Method }) {}
You can import the module with -Scope local to restrict a module to your script's scope. If the module happens to also be loaded in the global scope, then it will still be available after your script exits.