Skip to content

Commit 8ac161c

Browse files
author
Justin Grote
committed
Add rudimentary plan and whatif and better progress
1 parent 1bd2c3f commit 8ac161c

1 file changed

Lines changed: 46 additions & 56 deletions

File tree

ModuleFast.ps1

Lines changed: 46 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ THIS IS NOT FOR PRODUCTION, it should be considered "Fragile" and has very littl
2121
It also doesn't generate the PowershellGet XML files currently, so PowershellGet will see them as "External" modules
2222
#>
2323
function Install-ModuleFast {
24-
[CmdletBinding(SupportsShouldProcess)]
24+
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
2525
param(
2626
$ModulesToInstall,
2727
$Destination,
@@ -30,7 +30,6 @@ function Install-ModuleFast {
3030
[string]$Source = 'https://preview.pwsh.gallery/index.json',
3131
#The credential to use to authenticate. Only basic auth is supported
3232
[PSCredential]$Credential,
33-
[Switch]$Force,
3433
#By default will modify your PSModulePath to use the builtin destination if not present. Setting this implicitly skips profile update as well.
3534
[Switch]$NoPSModulePathUpdate,
3635
#Setting this won't add the default destination to your profile.
@@ -46,7 +45,7 @@ function Install-ModuleFast {
4645
# Autocreate the default as a convenience, otherwise require the path to be present to avoid mistakes
4746
if ($Destination -eq $defaultRepoPath -and -not (Test-Path $Destination)) {
4847
if ($PSCmdlet.ShouldProcess('Create Destination Folder', $Destination)) {
49-
New-Item -ItemType Directory -Path $Destination -Force
48+
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
5049
}
5150
}
5251

@@ -63,9 +62,30 @@ function Install-ModuleFast {
6362
Add-DestinationToPSModulePath -Destination $Destination -NoProfileUpdate:$NoProfileUpdate
6463
}
6564

65+
$currentWhatIfPreference = $WhatIfPreference
66+
#We do some stuff here that doesn't affect the system but triggers whatif, so we disable it
67+
$WhatIfPreference = $false
6668
$httpClient = New-ModuleFastClient -Credential $Credential
67-
Write-Progress -Id 1 -Activity 'Install-ModuleFast' -Status 'Preparing Plan' -PercentComplete 1
69+
Write-Progress -Id 1 -Activity 'Install-ModuleFast' -Status 'Plan' -PercentComplete 1
6870
$plan = Get-ModuleFastPlan $ModulesToInstall -HttpClient $httpClient -Source $Source
71+
$WhatIfPreference = $currentWhatIfPreference
72+
73+
if ($plan.Count -eq 0) {
74+
Write-Verbose 'No modules found to install or all modules are already installed. Exiting.'
75+
return
76+
}
77+
78+
if (-not $PSCmdlet.ShouldProcess($Destination, "Install $($plan.Count) Modules")) {
79+
Write-Host -fore DarkGreen '🚀 ModuleFast Install Plan BEGIN'
80+
$plan
81+
| Select-Object Name, @{N = 'Version'; E = { $_.Required } }
82+
| Format-Table -AutoSize
83+
| Out-String
84+
| Write-Host -ForegroundColor DarkGray
85+
Write-Host -fore DarkGreen '🚀 ModuleFast Install Plan END'
86+
return
87+
}
88+
6989

7090
Write-Progress -Id 1 -Activity 'Install-ModuleFast' -Status "Installing: $($plan.count) Modules" -PercentComplete 50
7191

@@ -136,7 +156,8 @@ function Get-ModuleFastPlan {
136156
#By default we use in-place modules if they satisfy the version requirements. This switch will force a search for all latest modules
137157
[Switch]$Update,
138158
[PSCredential]$Credential,
139-
[HttpClient]$HttpClient = $(New-ModuleFastClient -Credential $Credential)
159+
[HttpClient]$HttpClient = $(New-ModuleFastClient -Credential $Credential),
160+
[int]$ParentProgress
140161
)
141162

142163
BEGIN {
@@ -151,7 +172,6 @@ function Get-ModuleFastPlan {
151172
HttpClient = $httpClient
152173
CancellationToken = $cancelToken.Token
153174
}
154-
# Write-Progress -Id 1 -Activity 'Get-ModuleFast' -CurrentOperation 'Fetching module information from Powershell Gallery'
155175
}
156176
PROCESS {
157177
foreach ($spec in $Name) {
@@ -186,13 +206,18 @@ function Get-ModuleFastPlan {
186206
$currentTasks.Add($task)
187207
}
188208

209+
[int]$tasksCompleteCount = 1
210+
[int]$resolveTaskCount = $currentTasks.Count -as [Int]
189211
while ($currentTasks.Count -gt 0) {
190212
#The timeout here allow ctrl-C to continue working in PowerShell
191213
#-1 is returned by WaitAny if we hit the timeout before any tasks completed
192214
$noTasksYetCompleted = -1
193215
[int]$thisTaskIndex = [Task]::WaitAny($currentTasks, 500)
194216
if ($thisTaskIndex -eq $noTasksYetCompleted) { continue }
195217

218+
#The Plan whitespace is intentional so that it lines up with install progress using the compact format
219+
Write-Progress -Id 1 -Activity 'Install-ModuleFast' -Status "Plan: Resolving $tasksCompleteCount/$resolveTaskCount Module Dependencies" -PercentComplete ((($tasksCompleteCount / $resolveTaskCount) * 50) + 1)
220+
196221
#TODO: This only indicates headers were received, content may still be downloading and we dont want to block on that.
197222
#For now the content is small but this could be faster if we have another inner loop that WaitAny's on content
198223
#TODO: Perform a HEAD query to see if something has changed
@@ -299,6 +324,7 @@ function Get-ModuleFastPlan {
299324
#This should be a relatively rare query that only happens when the latest package isn't being resolved.
300325
[Task[string][]]$tasks = foreach ($page in $pages) {
301326
Get-ModuleInfoAsync @httpContext -Uri $page.'@id'
327+
#Used to track progress as tasks can get removed
302328
#This loop is here to support ctrl-c cancellation again
303329
}
304330
while ($false -in $tasks.IsCompleted) {
@@ -336,6 +362,7 @@ function Get-ModuleFastPlan {
336362
#TODO: Fix the flow so this isn't stated twice
337363
[void]$resolveTasks.Remove($completedTask)
338364
[void]$currentTasks.Remove($completedTask)
365+
$tasksCompleteCount++
339366
continue
340367
}
341368

@@ -415,12 +442,16 @@ function Get-ModuleFastPlan {
415442
Write-Debug "$currentModuleSpec`: Fetching dependency $dependencySpec"
416443
$task = Get-ModuleInfoAsync @httpContext -Endpoint $Source -Name $dependencySpec.Name
417444
$resolveTasks[$task] = $dependencySpec
445+
#Used to track progress as tasks can get removed
446+
$resolveTaskCount++
447+
418448
$currentTasks.Add($task)
419449
}
420450
}
421451
try {
422452
[void]$resolveTasks.Remove($completedTask)
423453
[void]$currentTasks.Remove($completedTask)
454+
$tasksCompleteCount++
424455
} catch {
425456
throw
426457
}
@@ -504,53 +535,8 @@ function Install-ModuleFastHelper {
504535
if (-not $installJobs.Remove($completedJob)) { throw 'Could not remove completed job from list. This is a bug, report it' }
505536
$installed++
506537
Write-Verbose "$installedModule`: Successfuly installed to $installPath"
507-
Write-Progress -Id $installProgressId -ParentId 1 -Activity 'Install' -Status "$installed/$($ModuleToInstall.count) Modules" -PercentComplete ($installed / $ModuleToInstall.count * 100)
538+
Write-Progress -Id 1 -Activity 'Install-ModuleFast' -Status "Install: $installed/$($ModuleToInstall.count) Modules" -PercentComplete ((($installed / $ModuleToInstall.count) * 50) + 50)
508539
}
509-
510-
# #Installation jobs are captured here, we will check them once all downloads have completed
511-
512-
513-
# $downloaded = 0
514-
# $downloadedProgressId = Get-Random
515-
# #TODO: Filestreams should be disposed in a try/catch in case of cancellation. In PS 7.3+, should be a clean() block
516-
# while ($downloadTasks.count -gt 0) {
517-
# #TODO: Check on in jobs and if there's a failure, cancel the rest of the jobs
518-
# $noTasksYetCompleted = -1
519-
# [int]$thisTaskIndex = [Task]::WaitAny($downloadTasks, 500)
520-
# if ($thisTaskIndex -eq $noTasksYetCompleted) { continue }
521-
# $thisTask = $downloadTasks[$thisTaskIndex]
522-
# $context = $taskMap[$thisTask]
523-
# # We can close these streams now that it is downloaded.
524-
# # This releases the lock on the file
525-
# #TODO: Maybe can connect the stream to a zip decompressionstream. Should be in cache so performance would be negligible
526-
# $context.fileStream.Dispose()
527-
# $context.fetchStream.Dispose()
528-
# #The file copy task is a void task that doesnt return anything, so we dont need to do GetResult()
529-
# $downloadTasks.RemoveAt($thisTaskIndex)
530-
531-
# #Start a new threadjob to handle the installation, because the zipfile API is not async. Also extraction is
532-
# #CPU intensive so multiple threads will be helpful here and worth the startup cost of a runspace
533-
# $installJobParams = @{
534-
# ScriptBlock = (Get-Item Function:\Install-ModuleFastOperation).Scriptblock
535-
# #Named parameters require a hack so we will just do these in order
536-
# ArgumentList = @(
537-
# $context.Module.Name,
538-
# $context.Module.Version,
539-
# $context.DownloadPath,
540-
# $Destination
541-
# )
542-
# }
543-
# Write-Debug "Starting Module Install Job for $($context.Module)"
544-
# $installJob = Start-ThreadJob @installJobParams
545-
# $installJobs.Add($installJob)
546-
# $downloaded++
547-
# Write-Progress -Id $downloadedProgressId -ParentId 1 -Activity 'Download' -Status "$downloaded/$($ModuleToInstall.count) Modules" -PercentComplete ($downloaded / $ModuleToInstall.count * 100)
548-
549-
# }
550-
551-
# #TODO: Correlate the installjobs to a dictionary so we can return the original modulespec maybe?
552-
# #Or is that even needed?
553-
554540
}
555541

556542
# This will be run inside a threadjob. We separate this so that we can test it independently
@@ -1075,7 +1061,7 @@ function Get-ModuleInfoAsync {
10751061
Adds an existing PowerShell Modules path to the current session as well as the profile
10761062
#>
10771063
function Add-DestinationToPSModulePath {
1078-
[CmdletBinding(SupportsShouldProcess)]
1064+
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
10791065
param(
10801066
[string]$Destination,
10811067
[switch]$NoProfileUpdate
@@ -1087,21 +1073,25 @@ function Add-DestinationToPSModulePath {
10871073
[string[]]$modulePaths = $env:PSModulePath -split [Path]::PathSeparator
10881074

10891075
if ($Destination -notin $modulePaths) {
1090-
$pathUpdateMessage = "Update PSModulePath $($NoProfileUpdate ? '' : 'and CurrentUserAllHosts profile ')to include $Destination"
1091-
if (-not $PSCmdlet.ShouldProcess($pathUpdateMessage, '', '')) { return }
1076+
Write-Verbose "Updating PSModulePath to include $Destination"
10921077
$modulePaths += $Destination
10931078
$env:PSModulePath = $modulePaths -join [Path]::PathSeparator
10941079
}
10951080

10961081
if (-not $NoProfileUpdate) {
1082+
#TODO: Support other profiles?
10971083
$myProfile = $profile.CurrentUserAllHosts
1084+
10981085
if (-not (Test-Path $myProfile)) {
1086+
if (-not $PSCmdlet.ShouldProcess($myProfile, "Allow ModuleFast to work by creating a profile at $myProfile.")) { return }
10991087
Write-Verbose 'User All Hosts profile not found, creating one.'
11001088
New-Item -ItemType File -Path $myProfile -Force | Out-Null
11011089
}
1102-
$ProfileLine = "`$env:PSModulePath += [System.IO.Path]::PathSeparator + $Destination #Added by ModuleFast. DO NOT EDIT THIS LINE. If you dont want this, add -NoProfileUpdate to your command."
1090+
$ProfileLine = "`$env:PSModulePath += '$([IO.Path]::PathSeparator + $Destination)' #Added by ModuleFast. DO NOT EDIT THIS LINE. If you do not want this, add -NoProfileUpdate to Install-ModuleFast."
11031091
if ((Get-Content -Raw $myProfile) -notmatch [Regex]::Escape($ProfileLine)) {
1092+
if (-not $PSCmdlet.ShouldProcess($myProfile, "Allow ModuleFast to work by adding $Destination to your PSModulePath on startup by appending to your CurrentUserAllHosts profile.")) { return }
11041093
Write-Verbose "Adding $Destination to profile $myProfile"
1094+
Add-Content -Path $myProfile -Value "`n`n"
11051095
Add-Content -Path $myProfile -Value $ProfileLine
11061096
} else {
11071097
Write-Verbose "PSModulePath $Destination already in profile, skipping..."

0 commit comments

Comments
 (0)