Skip to content

Commit 03c445b

Browse files
committed
🎨 Cleanup Errors and add Cache clear command
1 parent b5959b7 commit 03c445b

3 files changed

Lines changed: 61 additions & 36 deletions

File tree

ModuleFast.psm1

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
#requires -version 7.3
22
using namespace Microsoft.PowerShell.Commands
3-
using namespace System.Management.Automation
4-
using namespace System.Management.Automation.Language
53
using namespace NuGet.Versioning
64
using namespace System.Collections
75
using namespace System.Collections.Concurrent
@@ -10,13 +8,15 @@ using namespace System.Collections.Specialized
108
using namespace System.IO
119
using namespace System.IO.Compression
1210
using namespace System.IO.Pipelines
11+
using namespace System.Management.Automation
12+
using namespace System.Management.Automation.Language
1313
using namespace System.Net
1414
using namespace System.Net.Http
1515
using namespace System.Reflection
16+
using namespace System.Runtime.Caching
1617
using namespace System.Text
1718
using namespace System.Threading
1819
using namespace System.Threading.Tasks
19-
using namespace System.Runtime.Caching
2020

2121
#Because we are changing state, we want to be safe
2222
#TODO: Implement logic to only fail on module installs, such that one module failure doesn't prevent others from installing.
@@ -68,6 +68,11 @@ function Install-ModuleFast {
6868
6969
In addition, if you do not already have the %LOCALAPPDATA%\PowerShell\Modules in your $env:PSModulesPath, Modulefast will append a command to add it to your user profile. This is done to ensure that the modules are available for use in future sessions. If you do not want this behavior, you can specify the -NoProfileUpdate switch.
7070
71+
-------
72+
Caching
73+
-------
74+
ModuleFast will cache the results of the module selection process in memory for the duration of the PowerShell session. This is done to improve performance when multiple modules are being installed. If you want to clear the cache, you can call Clear-ModuleFastCache.
75+
7176
.PARAMETER WhatIf
7277
If specified, will output the installation plan to the pipeline as well as the console. This can be saved and provided to Install-ModuleFast at a later date.
7378
@@ -220,6 +225,8 @@ function Install-ModuleFast {
220225
[Switch]$PassThru
221226
)
222227
begin {
228+
trap {$PSCmdlet.ThrowTerminatingError($PSItem)}
229+
223230
# Setup the Destination repository
224231
$defaultRepoPath = $(Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'powershell/Modules')
225232

@@ -298,6 +305,8 @@ function Install-ModuleFast {
298305
}
299306

300307
end {
308+
trap {$PSCmdlet.ThrowTerminatingError($PSItem)}
309+
301310
if (-not $installPlan) {
302311
if ($ModulesToInstall.Count -eq 0 -and $PSCmdlet.ParameterSetName -eq 'Specification') {
303312
Write-Verbose '🔎 No modules specified to install. Beginning SpecFile detection...'
@@ -451,6 +460,8 @@ function Get-ModuleFastPlan {
451460
)
452461

453462
BEGIN {
463+
trap {$PSCmdlet.ThrowTerminatingError($PSItem)}
464+
454465
$ErrorActionPreference = 'Stop'
455466
[HashSet[ModuleFastSpec]]$modulesToResolve = @()
456467

@@ -466,13 +477,17 @@ function Get-ModuleFastPlan {
466477
}
467478
}
468479
PROCESS {
480+
trap {$PSCmdlet.ThrowTerminatingError($PSItem)}
481+
469482
foreach ($spec in $Specification) {
470483
if (-not $ModulesToResolve.Add($spec)) {
471484
Write-Warning "$spec was specified twice, skipping duplicate"
472485
}
473486
}
474487
}
475488
END {
489+
trap {$PSCmdlet.ThrowTerminatingError($PSItem)}
490+
476491
# A deduplicated list of modules to install
477492
[HashSet[ModuleFastInfo]]$modulesToInstall = @{}
478493

@@ -525,28 +540,28 @@ function Get-ModuleFastPlan {
525540
throw 'Failed to find Module Specification for completed task. This is a bug.'
526541
}
527542

528-
Write-Debug "$currentModuleSpec`: Processing Response"
543+
Write-Debug "${currentModuleSpec}: Processing Response"
529544
# We use GetAwaiter so we get proper error messages back, as things such as network errors might occur here.
530545
try {
531546
$response = $completedTask.GetAwaiter().GetResult()
532547
| ConvertFrom-Json
533-
Write-Debug "$currentModuleSpec`: Received Response with $($response.Count) pages"
548+
Write-Debug "${currentModuleSpec}: Received Response with $($response.Count) pages"
534549
} catch {
535550
$taskException = $PSItem.Exception.InnerException
536551
#TODO: Rewrite this as a handle filter
537552
if ($taskException -isnot [HttpRequestException]) { throw }
538553
[HttpRequestException]$err = $taskException
539554
if ($err.StatusCode -eq [HttpStatusCode]::NotFound) {
540-
throw [InvalidOperationException]"$currentModuleSpec`: module was not found in the $Source repository. Check the spelling and try again."
555+
throw [InvalidOperationException]"${currentModuleSpec}: module was not found in the $Source repository. Check the spelling and try again."
541556
}
542557

543558
#All other cases
544-
$PSItem.ErrorDetails = "$currentModuleSpec`: Failed to fetch module $currentModuleSpec from $Source. Error: $PSItem"
559+
$PSItem.ErrorDetails = "${currentModuleSpec}: Failed to fetch module $currentModuleSpec from $Source. Error: $PSItem"
545560
throw $PSItem
546561
}
547562

548563
if (-not $response.count) {
549-
throw [InvalidDataException]"$currentModuleSpec`: invalid result received from $Source. This is probably a bug. Content: $response"
564+
throw [InvalidDataException]"${currentModuleSpec}: invalid result received from $Source. This is probably a bug. Content: $response"
550565
}
551566

552567
#If what we are looking for exists in the response, we can stop looking
@@ -567,7 +582,7 @@ function Get-ModuleFastPlan {
567582
$selectedEntry = if ($entries) {
568583
#Sanity Check for Modules
569584
if ('ItemType:Script' -in $entries[0].tags) {
570-
throw [NotImplementedException]"$currentModuleSpec`: Script installations are currently not supported."
585+
throw [NotImplementedException]"${currentModuleSpec}: Script installations are currently not supported."
571586
}
572587

573588
[SortedSet[NuGetVersion]]$inlinedVersions = $entries.version
@@ -593,7 +608,7 @@ function Get-ModuleFastPlan {
593608
if ($selectedEntry.count -gt 1) { throw 'Multiple Entries Selected. This is a bug.' }
594609
#Search additional pages if we didn't find it in the inlined ones
595610
$selectedEntry ??= $(
596-
Write-Debug "$currentModuleSpec`: not found in inlined index. Determining appropriate page(s) to query"
611+
Write-Debug "${currentModuleSpec}: not found in inlined index. Determining appropriate page(s) to query"
597612

598613
#If not inlined, we need to find what page(s) might have the candidate info we are looking for, starting with the highest numbered page first
599614

@@ -606,10 +621,10 @@ function Get-ModuleFastPlan {
606621
| Sort-Object -Descending { [NuGetVersion]$PSItem.Upper }
607622

608623
if (-not $pages) {
609-
throw [InvalidOperationException]"$currentModuleSpec`: a matching module was not found in the $Source repository that satisfies the requested version constraints. You may need to specify -PreRelease or adjust your version constraints."
624+
throw [InvalidOperationException]"${currentModuleSpec}: a matching module was not found in the $Source repository that satisfies the requested version constraints. You may need to specify -PreRelease or adjust your version constraints."
610625
}
611626

612-
Write-Debug "$currentModuleSpec`: Found $(@($pages).Count) additional pages that might match the query: $($pages.'@id' -join ',')"
627+
Write-Debug "${currentModuleSpec}: Found $(@($pages).Count) additional pages that might match the query: $($pages.'@id' -join ',')"
613628

614629
#TODO: This is relatively slow and blocking, but we would need complicated logic to process it in the main task handler loop.
615630
#I really should make a pipeline that breaks off tasks based on the type of the response.
@@ -641,7 +656,7 @@ function Get-ModuleFastPlan {
641656
}
642657

643658
if ($currentModuleSpec.SatisfiedBy($candidate)) {
644-
Write-Debug "$currentModuleSpec`: Found satisfying version $candidate in the additional pages."
659+
Write-Debug "${currentModuleSpec}: Found satisfying version $candidate in the additional pages."
645660
$matchingEntry = $entries | Where-Object version -EQ $candidate
646661
if (-not $matchingEntry) { throw 'Multiple matching Entries found for a specific version. This is a bug and should not happen' }
647662
$matchingEntry
@@ -656,7 +671,7 @@ function Get-ModuleFastPlan {
656671
)
657672

658673
if (-not $selectedEntry) {
659-
throw [InvalidOperationException]"$currentModuleSpec`: a matching module was not found in the $Source repository that satisfies the version constraints. You may need to specify -PreRelease or adjust your version constraints."
674+
throw [InvalidOperationException]"${currentModuleSpec}: a matching module was not found in the $Source repository that satisfies the version constraints. You may need to specify -PreRelease or adjust your version constraints."
660675
}
661676
if (-not $selectedEntry.PackageContent) { throw "No package location found for $($selectedEntry.PackageContent). This should never happen and is a bug" }
662677

@@ -687,7 +702,7 @@ function Get-ModuleFastPlan {
687702
continue
688703
}
689704

690-
Write-Verbose "$selectedModule`: Added to install plan"
705+
Write-Verbose "${selectedModule}: Added to install plan"
691706

692707
# HACK: Pwsh doesn't care about target framework as of today so we can skip that evaluation
693708
# TODO: Should it? Should we check for the target framework and only install if it matches?
@@ -705,7 +720,7 @@ function Get-ModuleFastPlan {
705720

706721
[ModuleFastSpec]::new($PSItem.id, $range)
707722
}
708-
Write-Debug "$currentModuleSpec`: has $($dependencies.count) additional dependencies: $($dependencies -join ', ')"
723+
Write-Debug "${currentModuleSpec}: has $($dependencies.count) additional dependencies: $($dependencies -join ', ')"
709724

710725
# TODO: Where loop filter maybe
711726
[ModuleFastSpec[]]$dependenciesToResolve = $dependencies | Where-Object {
@@ -752,10 +767,8 @@ function Get-ModuleFastPlan {
752767
} else {
753768
Write-Debug "No local modules that satisfies dependency $dependencySpec. Checking Remote..."
754769
}
755-
# TODO: Deduplicate in-flight queries (az.accounts is a good example)
756-
# Write-Debug "$moduleSpec`: Checking if $dependencySpec already has an in-flight request that satisfies the requirement"
757770

758-
Write-Debug "$currentModuleSpec`: Fetching dependency $dependencySpec"
771+
Write-Debug "${currentModuleSpec}: Fetching dependency $dependencySpec"
759772
#TODO: Do a direct version lookup if the dependency is a required version
760773
$task = Get-ModuleInfoAsync @httpContext -Endpoint $Source -Name $dependencySpec.Name
761774
$taskSpecMap[$task] = $dependencySpec
@@ -784,6 +797,15 @@ function Get-ModuleFastPlan {
784797
}
785798
}
786799

800+
function Clear-ModuleFastCache {
801+
<#
802+
.SYNOPSIS
803+
Clears the ModuleFast HTTP Cache. This is useful if you are expecting a newer version of a module to be available.
804+
#>
805+
$SCRIPT:RequestCache.Dispose()
806+
$SCRIPT:RequestCache = [MemoryCache]::new('PowerShell-ModuleFast-RequestCache')
807+
}
808+
787809
#endregion Public
788810

789811
#region Private
@@ -821,7 +843,7 @@ function Install-ModuleFastHelper {
821843
$existingManifestPath = try {
822844
Resolve-Path (Join-Path $installPath "$($module.Name).psd1") -ErrorAction Stop
823845
} catch [ActionPreferenceStopException] {
824-
throw "$module`: Existing module folder found at $installPath but the manifest could not be found. This is likely a corrupted or missing module and should be fixed manually."
846+
throw "${module}: Existing module folder found at $installPath but the manifest could not be found. This is likely a corrupted or missing module and should be fixed manually."
825847
}
826848

827849
#TODO: Dedupe all import-powershelldatafile operations to a function ideally
@@ -851,7 +873,7 @@ function Install-ModuleFastHelper {
851873

852874
Write-Verbose "${module}: Downloading from $($module.Location)"
853875
if (-not $module.Location) {
854-
throw "$module`: No Download Link found. This is a bug"
876+
throw "${module}: No Download Link found. This is a bug"
855877
}
856878

857879
$streamTask = $httpClient.GetStreamAsync($module.Location, $CancellationToken)
@@ -920,7 +942,7 @@ function Install-ModuleFastHelper {
920942
$completedJobContext = $completedJob | Receive-Job -Wait -AutoRemoveJob
921943
if (-not $installJobs.Remove($completedJob)) { throw 'Could not remove completed job from list. This is a bug, report it' }
922944
$installed++
923-
Write-Verbose "$($completedJobContext.Module)`: Installed to $($completedJobContext.InstallPath)"
945+
Write-Verbose "$($completedJobContext.Module): Installed to $($completedJobContext.InstallPath)"
924946
Write-Progress -Id 1 -Activity 'Install-ModuleFast' -Status "Install: $installed/$($ModuleToInstall.count) Modules" -PercentComplete ((($installed / $ModuleToInstall.count) * 50) + 50)
925947
$context.Module.Location = $completedJobContext.InstallPath
926948
#Output the module for potential future passthru
@@ -1047,8 +1069,8 @@ class ModuleFastInfo: IComparable {
10471069
}
10481070

10491071
$ModuleFastInfoTypeData = @{
1050-
DefaultDisplayPropertySet = 'Name', 'ModuleVersion'
1051-
DefaultKeyPropertySet = 'Name', 'ModuleVersion'
1072+
DefaultDisplayPropertySet = 'Name', 'ModuleVersion', 'Location'
1073+
DefaultKeyPropertySet = 'Name', 'ModuleVersion', 'Location'
10521074
SerializationMethod = 'SpecificProperties'
10531075
PropertySerializationSet = 'Name', 'ModuleVersion', 'Location'
10541076
SerializationDepth = 0
@@ -1376,7 +1398,7 @@ function Get-ModuleInfoAsync {
13761398
if ($endpointTask) {
13771399
Write-Debug "REQUEST CACHE HIT for Registration Index $Endpoint"
13781400
} else {
1379-
Write-Debug ('{0}fetch registration index from {1}' -f ($ModuleId ? "$ModuleId`: " : ''), $Endpoint)
1401+
Write-Debug ('{0}fetch registration index from {1}' -f ($ModuleId ? "${ModuleId}: " : ''), $Endpoint)
13801402
$endpointTask = $HttpClient.GetStringAsync($Endpoint, $CancellationToken)
13811403
$SCRIPT:RequestCache[$Endpoint] = $endpointTask
13821404
}
@@ -1402,7 +1424,7 @@ function Get-ModuleInfoAsync {
14021424
#HACK: We need the task to be a unique reference for the context mapping that occurs later on, so this is an easy if obscure way to "clone" the task using PowerShell.
14031425
$requestTask = [Task]::WhenAll($requestTask)
14041426
} else {
1405-
Write-Debug ('{0}fetch info from {1}' -f ($ModuleId ? "$ModuleId`: " : ''), $uri)
1427+
Write-Debug ('{0}fetch info from {1}' -f ($ModuleId ? "${ModuleId}: " : ''), $uri)
14061428
$requestTask = $HttpClient.GetStringAsync($uri, $CancellationToken)
14071429
$SCRIPT:RequestCache[$Uri] = $requestTask
14081430
}
@@ -1724,7 +1746,7 @@ function Find-RequiredSpecFile ([string]$Path) {
17241746
}
17251747

17261748
if (-not $requireFiles) {
1727-
throw [NotSupportedException]"Could not find any required spec files in $Path. Verify the path is correct or specify Module Specifications either via -Path or -Specification"
1749+
throw [NotSupportedException]"Could not find any required spec files in $Path. Verify the path is correct or provide Module Specifications either via -Path or -Specification"
17281750
}
17291751
return $requireFiles
17301752
}
@@ -1824,4 +1846,4 @@ filter ConvertFrom-ModuleManifest {
18241846
# FIXME: DBops dependency version issue
18251847

18261848
Set-Alias imf -Value Install-ModuleFast
1827-
Export-ModuleMember -Function Get-ModuleFastPlan, Install-ModuleFast -Alias imf
1849+
Export-ModuleMember -Function Get-ModuleFastPlan, Install-ModuleFast, Clear-ModuleFastCache -Alias imf

README.MD

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ iwr bit.ly/modulefast | iex
1515
Install-ModuleFast ImportExcel
1616
```
1717

18+
Modulefast is also accessible via the alias `imf` once loaded
19+
1820
The bit.ly link will always point to the latest release of ModuleFast by default. In order to avoid breaking changes, you can pin to a specific release. This is recommended when using CI systems such as GitHub Actions to install dependencies but is generally not needed on an interactive basis.
1921

2022
### View the Detailed Help for ModuleFast
@@ -76,11 +78,15 @@ You can alternatively set it as your `powershell.config.json` PSModulePath, add
7678

7779
## Goals
7880

79-
* Given a set of packages, install the packages and their dependencies as fast as possible in a declarative plan/apply incremental approach.
80-
* Packages once deployed should be fully compatible with built-in *-Module commands, PSGetv2, and PSGetv3.
81-
* Support a "plan" view that shows what will change before it changes, Terraform-style
82-
* Support a dependencies file that can declaratively define what packages a module should have
83-
* Support a "complete" mode that will clean up packages that aren't the latest versions of the specified modules.
81+
* [x] Given a set of packages, install the packages and their dependencies as fast as possible in a declarative plan/apply incremental approach.* Support a "plan" view that shows what will change before it changes, Terraform-style
82+
* [x] Support a "plan" view that shows what will change before it changes, Terraform-style
83+
* [x] Support a dependencies file that can declaratively define what packages a module should have
84+
* [x] Packages once deployed should be fully compatible with built-in *-Module commands, PSGetv2, and PSGetv3
85+
* [x] Support Third Party NuGet v3 Repositories using HTTPS Basic Auth (PAT Tokens etc.)
86+
* [x] Install requirements for scripts with #Requires statements
87+
* [ ] Able to use existing PSResourceGet registrations if NuGet v3 Repositories
88+
89+
~~Support a "complete" mode that will clean up packages that aren't the latest versions of the specified modules~~ Decided there isn't much value for this if packages are correct
8490

8591
## Non-Goals
8692

requires.lock.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)