HostedEnvironment.cs (22631B)
1 // ----------------------------------------------------------------------------- 2 // <copyright file="HostedEnvironment.cs" company="Microsoft Corporation"> 3 // Copyright (c) Microsoft Corporation. Licensed under the MIT License. 4 // </copyright> 5 // ----------------------------------------------------------------------------- 6 7 namespace Microsoft.Management.Configuration.Processor.PowerShell.Runspaces 8 { 9 using System; 10 using System.Collections; 11 using System.Collections.Generic; 12 using System.IO; 13 using System.Linq; 14 using System.Management.Automation; 15 using System.Management.Automation.Runspaces; 16 using System.Runtime.InteropServices.WindowsRuntime; 17 using Microsoft.Management.Configuration.Processor.Constants; 18 using Microsoft.Management.Configuration.Processor.Helpers; 19 using Microsoft.Management.Configuration.Processor.PowerShell.DscModules; 20 using Microsoft.Management.Configuration.Processor.PowerShell.DscResourcesInfo; 21 using Microsoft.Management.Configuration.Processor.PowerShell.Helpers; 22 using Microsoft.Management.Configuration.Processor.PowerShell.ProcessorEnvironments; 23 using Microsoft.PowerShell.Commands; 24 using Windows.Foundation.Collections; 25 using Windows.Security.Cryptography.Certificates; 26 using Windows.Storage.Streams; 27 using static Microsoft.Management.Configuration.Processor.PowerShell.Constants.PowerShellConstants; 28 29 /// <summary> 30 /// Process environment. Provides interaction with PowerShell for a hosted environment. 31 /// </summary> 32 internal class HostedEnvironment : IProcessorEnvironment 33 { 34 private readonly PowerShellConfigurationProcessorType type; 35 private readonly IPowerShellGet powerShellGet; 36 37 private PowerShellConfigurationProcessorLocation location = PowerShellConfigurationProcessorLocation.CurrentUser; 38 private string? customLocation; 39 40 /// <summary> 41 /// Initializes a new instance of the <see cref="HostedEnvironment"/> class. 42 /// </summary> 43 /// <param name="runspace">PowerShell Runspace.</param> 44 /// <param name="type">Configuration processor type.</param> 45 /// <param name="dscModule">IDscModule.</param> 46 public HostedEnvironment( 47 Runspace runspace, 48 PowerShellConfigurationProcessorType type, 49 IDscModule dscModule) 50 { 51 this.Runspace = runspace; 52 this.type = type; 53 this.DscModule = dscModule; 54 55 // TODO: once v3 is release implement v3 version. 56 this.powerShellGet = new PowerShellGetV2(); 57 } 58 59 /// <inheritdoc/> 60 public Runspace Runspace { get; } 61 62 /// <summary> 63 /// Gets the DscModule. 64 /// </summary> 65 internal IDscModule DscModule { get; } 66 67 /// <summary> 68 /// Gets or initializes the set processor factory. 69 /// </summary> 70 internal PowerShellConfigurationSetProcessorFactory? SetProcessorFactory { get; init; } 71 72 /// <inheritdoc/> 73 public void ValidateRunspace() 74 { 75 // Only support PowerShell Core. 76 if (this.GetVariable<string>(Variables.PSEdition) != Core) 77 { 78 throw new NotSupportedException("Only PowerShell Core is supported."); 79 } 80 81 // If opening a runspace has failures, like one of the modules in ImportPSModule is not found, it won't throw but 82 // write to the error output. This is not a fatal error, since we install PSDesiredStateConfiguration 83 // module if not found, so unless there's a real reason keep it in verbose. 84 var errors = this.GetVariable<ArrayList>(Variables.Error); 85 if (errors.Count > 0) 86 { 87 this.OnDiagnostics( 88 DiagnosticLevel.Verbose, 89 $"Error creating runspace '{string.Join("\n", errors.Cast<string>().ToArray())}'"); 90 } 91 92 var powerShellGet = PowerShellHelpers.CreateModuleSpecification( 93 Modules.PowerShellGet, 94 minVersion: Modules.PowerShellGetMinVersion); 95 if (!this.ValidateModule(powerShellGet)) 96 { 97 var previousVersion = this.GetAvailableModule( 98 PowerShellHelpers.CreateModuleSpecification( 99 Modules.PowerShellGet)); 100 string message = $"Required '{powerShellGet}'"; 101 if (previousVersion is not null) 102 { 103 message += $" Found '{previousVersion.Name} {previousVersion.Version}'"; 104 } 105 106 throw new NotSupportedException(message); 107 } 108 109 // Make sure PSDesiredConfiguration is present. 110 this.InstallModule(this.DscModule.ModuleSpecification); 111 } 112 113 /// <inheritdoc/> 114 public IReadOnlyList<DscResourceInfoInternal> GetAllDscResources() 115 { 116 using PowerShell pwsh = PowerShell.Create(this.Runspace); 117 var results = this.DscModule.GetAllDscResources(pwsh); 118 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 119 return results; 120 } 121 122 /// <inheritdoc/> 123 public IReadOnlyList<DscResourceInfoInternal> GetDscResourcesInModule(ModuleSpecification moduleSpecification) 124 { 125 using PowerShell pwsh = PowerShell.Create(this.Runspace); 126 var results = this.DscModule.GetDscResourcesInModule(pwsh, moduleSpecification); 127 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 128 return results; 129 } 130 131 /// <inheritdoc/> 132 public DscResourceInfoInternal? GetDscResource(ConfigurationUnitAndModule unitInternal) 133 { 134 using PowerShell pwsh = PowerShell.Create(this.Runspace); 135 var result = this.DscModule.GetDscResource(pwsh, unitInternal.ResourceName, unitInternal.Module); 136 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 137 return result; 138 } 139 140 /// <inheritdoc/> 141 public ValueSet InvokeGetResource(ValueSet settings, string name, ModuleSpecification? moduleSpecification) 142 { 143 using PowerShell pwsh = PowerShell.Create(this.Runspace); 144 var result = this.DscModule.InvokeGetResource(pwsh, settings, name, moduleSpecification); 145 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 146 return result; 147 } 148 149 /// <inheritdoc/> 150 public bool InvokeTestResource(ValueSet settings, string name, ModuleSpecification? moduleSpecification) 151 { 152 using PowerShell pwsh = PowerShell.Create(this.Runspace); 153 var result = this.DscModule.InvokeTestResource(pwsh, settings, name, moduleSpecification); 154 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 155 return result; 156 } 157 158 /// <inheritdoc/> 159 public bool InvokeSetResource(ValueSet settings, string name, ModuleSpecification? moduleSpecification) 160 { 161 using PowerShell pwsh = PowerShell.Create(this.Runspace); 162 var result = this.DscModule.InvokeSetResource(pwsh, settings, name, moduleSpecification); 163 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 164 return result; 165 } 166 167 /// <inheritdoc/> 168 public PSModuleInfo? GetImportedModule(ModuleSpecification moduleSpecification) 169 { 170 using PowerShell pwsh = PowerShell.Create(this.Runspace); 171 172 var moduleInfo = pwsh.AddCommand(Commands.GetModule) 173 .AddParameter(Parameters.FullyQualifiedName, moduleSpecification) 174 .Invoke<PSModuleInfo>() 175 .FirstOrDefault(); 176 177 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 178 return moduleInfo; 179 } 180 181 /// <inheritdoc/> 182 public PSModuleInfo? GetAvailableModule(ModuleSpecification moduleSpecification) 183 { 184 using PowerShell pwsh = PowerShell.Create(this.Runspace); 185 186 var moduleInfo = pwsh.AddCommand(Commands.GetModule) 187 .AddParameter(Parameters.FullyQualifiedName, moduleSpecification) 188 .AddParameter(Parameters.ListAvailable) 189 .Invoke<PSModuleInfo>() 190 .FirstOrDefault(); 191 192 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 193 return moduleInfo; 194 } 195 196 /// <inheritdoc/> 197 public PSModuleInfo? GetAvailableModule(string path) 198 { 199 using PowerShell pwsh = PowerShell.Create(this.Runspace); 200 201 var moduleInfo = pwsh.AddCommand(Commands.GetModule) 202 .AddParameter(Parameters.Name, path) 203 .AddParameter(Parameters.ListAvailable) 204 .Invoke<PSModuleInfo>() 205 .FirstOrDefault(); 206 207 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 208 return moduleInfo; 209 } 210 211 /// <inheritdoc/> 212 public void ImportModule(ModuleSpecification moduleSpecification) 213 { 214 using PowerShell pwsh = PowerShell.Create(this.Runspace); 215 216 _ = pwsh.AddCommand(Commands.ImportModule) 217 .AddParameter(Parameters.FullyQualifiedName, moduleSpecification) 218 .Invoke(); 219 220 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 221 } 222 223 /// <inheritdoc/> 224 public void ImportModule(string path) 225 { 226 if (!File.Exists(path)) 227 { 228 throw new FileNotFoundException(path); 229 } 230 231 using PowerShell pwsh = PowerShell.Create(this.Runspace); 232 233 _ = pwsh.AddCommand(Commands.ImportModule) 234 .AddParameter(Parameters.Name, path) 235 .Invoke(); 236 237 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 238 } 239 240 /// <inheritdoc/> 241 public PSObject? GetInstalledModule(ModuleSpecification moduleSpecification) 242 { 243 // Instead of Get-InstalledModule, we look for PSGetModuleInfo.xml and serialize it 244 // if found. This allow us to get the information from Install-Module and Save-Module. 245 var module = this.GetAvailableModule(moduleSpecification); 246 if (module is null) 247 { 248 return null; 249 } 250 251 var getModuleInfoFile = Path.Combine(module.ModuleBase, "PSGetModuleInfo.xml"); 252 if (!File.Exists(getModuleInfoFile)) 253 { 254 // Keep Get-InstalledModule behaviour. 255 return null; 256 } 257 258 using PowerShell pwsh = PowerShell.Create(this.Runspace); 259 var installedModule = pwsh.AddCommand(Commands.ImportCliXml) 260 .AddParameter(Parameters.Path, getModuleInfoFile) 261 .Invoke() 262 .FirstOrDefault(); 263 264 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 265 return installedModule; 266 } 267 268 /// <inheritdoc/> 269 public PSObject? FindModule(ConfigurationUnitInternal unitInternal) 270 { 271 // Don't use ModuleSpecification here. Each parameter is independent and 272 // we need version even if a module was not specified. 273 string? moduleName = unitInternal.GetDirective<string>(DirectiveConstants.Module); 274 275 if (string.IsNullOrEmpty(moduleName)) 276 { 277 return null; 278 } 279 280 using PowerShell pwsh = PowerShell.Create(this.Runspace); 281 282 var result = this.powerShellGet.FindModule( 283 pwsh, 284 moduleName, 285 unitInternal.GetSemanticVersion(), 286 unitInternal.GetSemanticMinVersion(), 287 unitInternal.GetSemanticMaxVersion(), 288 unitInternal.GetDirective<string>(DirectiveConstants.Repository), 289 unitInternal.GetDirective(DirectiveConstants.AllowPrerelease)); 290 291 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 292 return result; 293 } 294 295 /// <inheritdoc/> 296 public PSObject? FindDscResource(ConfigurationUnitInternal unitInternal) 297 { 298 using PowerShell pwsh = PowerShell.Create(this.Runspace); 299 300 var result = this.powerShellGet.FindDscResource( 301 pwsh, 302 unitInternal.ResourceName, 303 unitInternal.GetDirective<string>(DirectiveConstants.Module), 304 unitInternal.GetSemanticVersion(), 305 unitInternal.GetSemanticMinVersion(), 306 unitInternal.GetSemanticMaxVersion(), 307 unitInternal.GetDirective<string>(DirectiveConstants.Repository), 308 unitInternal.GetDirective(DirectiveConstants.AllowPrerelease)); 309 310 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 311 return result; 312 } 313 314 /// <inheritdoc/> 315 public void SaveModule(PSObject inputObject, string location) 316 { 317 using PowerShell pwsh = PowerShell.Create(this.Runspace); 318 this.powerShellGet.SaveModule(pwsh, inputObject, location); 319 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 320 } 321 322 /// <inheritdoc/> 323 public void SaveModule(ModuleSpecification moduleSpecification, string location) 324 { 325 using PowerShell pwsh = PowerShell.Create(this.Runspace); 326 this.powerShellGet.SaveModule(pwsh, moduleSpecification, location); 327 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 328 } 329 330 /// <inheritdoc/> 331 public void InstallModule(PSObject inputObject) 332 { 333 if (this.location == PowerShellConfigurationProcessorLocation.Custom) 334 { 335 if (string.IsNullOrEmpty(this.customLocation)) 336 { 337 throw new ArgumentNullException(nameof(this.customLocation)); 338 } 339 340 this.SaveModule(inputObject, this.customLocation); 341 } 342 else 343 { 344 using PowerShell pwsh = PowerShell.Create(this.Runspace); 345 this.powerShellGet.InstallModule(pwsh, inputObject, this.location == PowerShellConfigurationProcessorLocation.AllUsers); 346 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 347 } 348 } 349 350 /// <inheritdoc/> 351 public void InstallModule(ModuleSpecification moduleSpecification) 352 { 353 // Maybe is already there. 354 if (!this.ValidateModule(moduleSpecification)) 355 { 356 this.OnDiagnostics(DiagnosticLevel.Verbose, $"Installing module: {moduleSpecification.Name} ..."); 357 358 // Ok, we have to get it. 359 if (this.location == PowerShellConfigurationProcessorLocation.Custom) 360 { 361 if (string.IsNullOrEmpty(this.customLocation)) 362 { 363 throw new ArgumentNullException(nameof(this.customLocation)); 364 } 365 366 this.OnDiagnostics(DiagnosticLevel.Verbose, $"... calling save module ..."); 367 this.SaveModule(moduleSpecification, this.customLocation); 368 } 369 else 370 { 371 this.OnDiagnostics(DiagnosticLevel.Verbose, $"... calling install module ..."); 372 using PowerShell pwsh = PowerShell.Create(this.Runspace); 373 this.powerShellGet.InstallModule(pwsh, moduleSpecification, this.location == PowerShellConfigurationProcessorLocation.AllUsers); 374 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 375 } 376 377 this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module installed."); 378 } 379 } 380 381 /// <inheritdoc/> 382 public List<Certificate> GetCertsOfValidSignedFiles(string[] paths) 383 { 384 using PowerShell pwsh = PowerShell.Create(this.Runspace); 385 386 var signatures = pwsh.AddCommand(Commands.GetChildItem) 387 .AddParameter(Parameters.Path, paths) 388 .AddCommand(Commands.GetAuthenticodeSignature) 389 .Invoke<Signature>(); 390 391 var thumbprint = new HashSet<string>(); 392 var certificates = new List<Certificate>(); 393 foreach (var signature in signatures) 394 { 395 if (signature.Status == SignatureStatus.Valid) 396 { 397 if (thumbprint.Add(signature.SignerCertificate.Thumbprint)) 398 { 399 IBuffer buffer = signature.SignerCertificate.GetRawCertData().AsBuffer(); 400 certificates.Add(new Certificate(buffer)); 401 } 402 } 403 } 404 405 this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); 406 return certificates; 407 } 408 409 /// <inheritdoc/> 410 public TType GetVariable<TType>(string name) 411 { 412 return (TType)this.Runspace.SessionStateProxy.PSVariable.GetValue(name); 413 } 414 415 /// <inheritdoc/> 416 public void SetVariable(string name, object value) 417 { 418 this.Runspace.SessionStateProxy.PSVariable.Set(name, value); 419 } 420 421 /// <inheritdoc/> 422 public void SetPSModulePath(string path) 423 { 424 this.SetVariable(Variables.PSModulePath, path); 425 } 426 427 /// <inheritdoc/> 428 public void SetPSModulePaths(IReadOnlyList<string> paths) 429 { 430 this.SetVariable(Variables.PSModulePath, string.Join(";", paths)); 431 } 432 433 /// <inheritdoc/> 434 public void PrependPSModulePath(string path) 435 { 436 var oldModulePath = this.GetModulePaths(); 437 if (!oldModulePath.Contains(path)) 438 { 439 this.SetPSModulePath($"{path};{string.Join(";", oldModulePath)}"); 440 } 441 } 442 443 /// <inheritdoc/> 444 public void PrependPSModulePaths(IReadOnlyList<string> paths) 445 { 446 var newPaths = paths.ToList(); 447 var oldModulePath = this.GetModulePaths(); 448 foreach (var newPath in paths) 449 { 450 if (oldModulePath.Contains(newPath)) 451 { 452 newPaths.Remove(newPath); 453 } 454 } 455 456 if (newPaths.Any()) 457 { 458 this.SetPSModulePath($"{string.Join(";", newPaths)};{string.Join(";", oldModulePath)}"); 459 } 460 } 461 462 /// <inheritdoc/> 463 public void AppendPSModulePath(string path) 464 { 465 var oldModulePath = this.GetModulePaths(); 466 if (!oldModulePath.Contains(path)) 467 { 468 this.SetPSModulePath($"{string.Join(";", oldModulePath)};{path}"); 469 } 470 } 471 472 /// <inheritdoc/> 473 public void AppendPSModulePaths(IReadOnlyList<string> paths) 474 { 475 var newPaths = paths.ToList(); 476 var oldModulePath = this.GetModulePaths(); 477 foreach (var newPath in paths) 478 { 479 if (oldModulePath.Contains(newPath)) 480 { 481 newPaths.Remove(newPath); 482 } 483 } 484 485 if (newPaths.Any()) 486 { 487 this.SetPSModulePath($"{string.Join(";", oldModulePath)};{string.Join(";", newPaths)}"); 488 } 489 } 490 491 /// <inheritdoc/> 492 public void CleanupPSModulePath(string path) 493 { 494 string newModulePath = this.GetVariable<string>(Variables.PSModulePath) 495 .Replace($"{path};", null) 496 .Replace($";{path}", null); 497 498 this.SetPSModulePath(newModulePath); 499 } 500 501 /// <inheritdoc/> 502 public void SetLocation(PowerShellConfigurationProcessorLocation location, string? customLocation) 503 { 504 this.location = location; 505 if (this.location == PowerShellConfigurationProcessorLocation.Custom) 506 { 507 if (string.IsNullOrEmpty(customLocation)) 508 { 509 throw new ArgumentNullException(nameof(customLocation)); 510 } 511 512 this.customLocation = customLocation; 513 } 514 } 515 516 private bool ValidateModule(ModuleSpecification moduleSpecification) 517 { 518 this.OnDiagnostics(DiagnosticLevel.Verbose, $"Validating module: {moduleSpecification.Name} ..."); 519 520 var loadedModule = this.GetImportedModule(moduleSpecification); 521 if (loadedModule is not null) 522 { 523 this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module is already imported."); 524 return true; 525 } 526 527 var availableModule = this.GetAvailableModule(moduleSpecification); 528 if (availableModule is not null) 529 { 530 this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module is available, importing ..."); 531 this.ImportModule(moduleSpecification); 532 this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module imported."); 533 return true; 534 } 535 536 this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module not found."); 537 return false; 538 } 539 540 private void OnDiagnostics(DiagnosticLevel level, PowerShell pwsh) 541 { 542 this.SetProcessorFactory?.OnDiagnostics(level, pwsh); 543 } 544 545 private void OnDiagnostics(DiagnosticLevel level, string message) 546 { 547 this.SetProcessorFactory?.OnDiagnostics(level, message); 548 } 549 550 private HashSet<string> GetModulePaths() 551 { 552 return this.GetVariable<string>(Variables.PSModulePath).Split(";").ToHashSet<string>(); 553 } 554 } 555 }