diff --git a/.vscode/launch.json b/.vscode/launch.json index 251413c5d1..79c6a21b3c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -74,6 +74,18 @@ "cwd": "${workspaceFolder}", "stopAtEntry": false, "console": "integratedTerminal" + }, + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/dist/**/*.js" + ], + "preLaunchTask": "Build vscode" } ] } diff --git a/PSRule.sln b/PSRule.sln index 9374f455dd..61f1f9d895 100644 --- a/PSRule.sln +++ b/PSRule.sln @@ -36,8 +36,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.CommandLine.Tests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Types.Tests", "tests\PSRule.Types.Tests\PSRule.Types.Tests.csproj", "{34095F78-CDA3-4E72-B64C-6366EA4B3EAF}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2E2A23CD-BD35-45DE-B2BD-20C8E45BBA41}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSRule.EditorServices", "src\PSRule.EditorServices\PSRule.EditorServices.csproj", "{061DD38A-B9E9-4EF1-B5B7-D0A484DB74D1}" EndProject Global @@ -46,6 +44,14 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Release|Any CPU.Build.0 = Release|Any CPU + {309BED8B-4E60-4C42-A2B4-37A2E7EBEF3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {309BED8B-4E60-4C42-A2B4-37A2E7EBEF3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {309BED8B-4E60-4C42-A2B4-37A2E7EBEF3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {309BED8B-4E60-4C42-A2B4-37A2E7EBEF3F}.Release|Any CPU.Build.0 = Release|Any CPU {0130215D-58EB-4887-B6FA-31ED02500569}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0130215D-58EB-4887-B6FA-31ED02500569}.Debug|Any CPU.Build.0 = Debug|Any CPU {0130215D-58EB-4887-B6FA-31ED02500569}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -58,10 +64,6 @@ Global {D3488CE2-779F-4474-B38A-F894A4B689F7}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3488CE2-779F-4474-B38A-F894A4B689F7}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3488CE2-779F-4474-B38A-F894A4B689F7}.Release|Any CPU.Build.0 = Release|Any CPU - {309BED8B-4E60-4C42-A2B4-37A2E7EBEF3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {309BED8B-4E60-4C42-A2B4-37A2E7EBEF3F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {309BED8B-4E60-4C42-A2B4-37A2E7EBEF3F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {309BED8B-4E60-4C42-A2B4-37A2E7EBEF3F}.Release|Any CPU.Build.0 = Release|Any CPU {20DDCC65-8A9A-4BDC-91EC-C3BE6F32E52E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {20DDCC65-8A9A-4BDC-91EC-C3BE6F32E52E}.Debug|Any CPU.Build.0 = Debug|Any CPU {20DDCC65-8A9A-4BDC-91EC-C3BE6F32E52E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -74,10 +76,6 @@ Global {BDDBFDB8-614F-4B8A-930C-DCB60144598C}.Debug|Any CPU.Build.0 = Debug|Any CPU {BDDBFDB8-614F-4B8A-930C-DCB60144598C}.Release|Any CPU.ActiveCfg = Release|Any CPU {BDDBFDB8-614F-4B8A-930C-DCB60144598C}.Release|Any CPU.Build.0 = Release|Any CPU - {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Release|Any CPU.Build.0 = Release|Any CPU {872D2648-2F00-475E-84B5-F08BE07385B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {872D2648-2F00-475E-84B5-F08BE07385B7}.Debug|Any CPU.Build.0 = Debug|Any CPU {872D2648-2F00-475E-84B5-F08BE07385B7}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -111,7 +109,6 @@ Global {DA46C891-08F1-4D01-9F98-1F8BB10CAFEC} = {E0EA0CBA-96C5-4447-8B69-BC13EF0D7A4A} {C25E2FC1-E306-4D99-925C-15E5DD51F6A2} = {E0EA0CBA-96C5-4447-8B69-BC13EF0D7A4A} {34095F78-CDA3-4E72-B64C-6366EA4B3EAF} = {E0EA0CBA-96C5-4447-8B69-BC13EF0D7A4A} - {061DD38A-B9E9-4EF1-B5B7-D0A484DB74D1} = {2E2A23CD-BD35-45DE-B2BD-20C8E45BBA41} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {533491EB-BAE9-472E-B57F-A675ECD335B5} diff --git a/docs/CHANGELOG-v3.md b/docs/CHANGELOG-v3.md index e292b7c79f..1723898208 100644 --- a/docs/CHANGELOG-v3.md +++ b/docs/CHANGELOG-v3.md @@ -27,6 +27,18 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v3.0.0-B0340: + +- New features: + - VSCode extension includes PSRule runtime by @BernieWhite. + [#1755](https://github.com/microsoft/PSRule/issues/1755) + - The PSRule runtime is bundled with the VSCode extension. + - Separate installation of the PSRule PowerShell module is no longer required. + - VSCode extension asks to automatically restore modules by @BernieWhite. + [#2642](https://github.com/microsoft/PSRule/issues/2642) + - When opening a workspace, the extension will ask to restore any modules from the lock file. + - Alternatively, running the `PSRule: Restore modules` command manually will restore modules. + ## v3.0.0-B0340 (pre-release) What's changed since pre-release v3.0.0-B0315: diff --git a/package.json b/package.json index 8032e2c934..87d93f565c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "onLanguage:powershell", "onLanguage:yaml", "workspaceContains:/ps-rule.yaml", + "workspaceContains:/ps-rule.lock.json", "workspaceContains:**/ps-rule.yaml", "workspaceContains:**/*.Rule.yaml", "workspaceContains:**/*.Rule.yml", @@ -196,16 +197,29 @@ "description": "Enables experimental features in the PSRule extension.", "scope": "application" }, + "PSRule.lock.restore": { + "type": "boolean", + "default": false, + "description": "Determines if workspace modules will automatically be restored during activation. Modules can be restored manually using the PSRule: Restore modules command.", + "markdownDescription": "Determines if workspace modules will automatically be restored during activation. Modules can be restored manually using the `PSRule: Restore modules` command.", + "scope": "window" + }, "PSRule.notifications.showChannelUpgrade": { "type": "boolean", "default": true, - "description": "Determines if a notification to switch to the stable channel is shown on start up.", + "description": "Determines if a notification to switch to the stable channel is shown on activation.", + "scope": "application" + }, + "PSRule.notifications.showModuleRestore": { + "type": "boolean", + "default": true, + "description": "Determines if a notification to restore modules is shown on activation.", "scope": "application" }, "PSRule.notifications.showPowerShellExtension": { "type": "boolean", "default": true, - "description": "Determines if a notification to install the PowerShell extension is shown on start up.", + "description": "Determines if a notification to install the PowerShell extension is shown on activation.", "scope": "application" }, "PSRule.options.path": { diff --git a/src/PSRule.CommandLine/ClientContext.cs b/src/PSRule.CommandLine/ClientContext.cs index 544c127ae8..0d68525a0d 100644 --- a/src/PSRule.CommandLine/ClientContext.cs +++ b/src/PSRule.CommandLine/ClientContext.cs @@ -61,7 +61,8 @@ public ClientContext(InvocationContext invocation, string? option, bool verbose, public bool Debug { get; } /// - /// Configures the path to use for caching rules modules. + /// Configures the root path to use for caching artifacts including modules. + /// Each artifact is in a subdirectory of the root path. /// public string CachePath { get; } diff --git a/src/PSRule.CommandLine/ClientHost.cs b/src/PSRule.CommandLine/ClientHost.cs index 056ff31e10..ca500d1c96 100644 --- a/src/PSRule.CommandLine/ClientHost.cs +++ b/src/PSRule.CommandLine/ClientHost.cs @@ -135,4 +135,7 @@ public override void Debug(string text) _Context.Invocation.Console.WriteLine(text); } + + /// + public override string? CachePath => _Context.CachePath; } diff --git a/src/PSRule.EditorServices/ClientBuilder.cs b/src/PSRule.EditorServices/ClientBuilder.cs index 96bc77d4ff..3fe5549444 100644 --- a/src/PSRule.EditorServices/ClientBuilder.cs +++ b/src/PSRule.EditorServices/ClientBuilder.cs @@ -37,6 +37,7 @@ internal sealed class ClientBuilder private readonly Option _Run_Module; private readonly Option _Run_Baseline; private readonly Option _Run_Outcome; + private readonly Option _Run_NoRestore; private ClientBuilder(RootCommand cmd) { @@ -87,6 +88,10 @@ private ClientBuilder(RootCommand cmd) description: CmdStrings.Run_Outcome_Description ).FromAmong("Pass", "Fail", "Error", "Processed", "Problem"); _Run_Outcome.Arity = ArgumentArity.ZeroOrMore; + _Run_NoRestore = new Option( + "--no-restore", + description: CmdStrings.Run_NoRestore_Description + ); // Options for the module command. _Module_Init_Force = new Option( @@ -144,6 +149,7 @@ private void AddRun() cmd.AddOption(_Run_Module); cmd.AddOption(_Run_Baseline); cmd.AddOption(_Run_Outcome); + cmd.AddOption(_Run_NoRestore); cmd.SetHandler(async (invocation) => { var option = new RunOptions @@ -153,6 +159,7 @@ private void AddRun() Module = invocation.ParseResult.GetValueForOption(_Run_Module), Baseline = invocation.ParseResult.GetValueForOption(_Run_Baseline), Outcome = ParseOutcome(invocation.ParseResult.GetValueForOption(_Run_Outcome)), + NoRestore = invocation.ParseResult.GetValueForOption(_Run_NoRestore), }; var client = GetClientContext(invocation); @@ -164,6 +171,7 @@ private void AddRun() invocation.Console.WriteLine($"VERBOSE: Using workspace: {Environment.GetWorkingPath()}"); invocation.Console.WriteLine($"VERBOSE: Using module search path: {sp}"); invocation.Console.WriteLine($"VERBOSE: Using language server: {client.Path}"); + invocation.Console.WriteLine($"VERBOSE: Using cache path: {client.CachePath}"); } invocation.ExitCode = await RunCommand.RunAsync(option, client); diff --git a/src/PSRule.EditorServices/Resources/CmdStrings.Designer.cs b/src/PSRule.EditorServices/Resources/CmdStrings.Designer.cs index ef26633884..462264a5ab 100644 --- a/src/PSRule.EditorServices/Resources/CmdStrings.Designer.cs +++ b/src/PSRule.EditorServices/Resources/CmdStrings.Designer.cs @@ -258,6 +258,15 @@ internal static string Run_Module_Description { } } + /// + /// Looks up a localized string similar to Do not restore modules before running rules.. + /// + internal static string Run_NoRestore_Description { + get { + return ResourceManager.GetString("Run_NoRestore_Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Specifies the rule results to show in output. By default, Pass/ Fail/ Error results are shown.. /// diff --git a/src/PSRule.EditorServices/Resources/CmdStrings.resx b/src/PSRule.EditorServices/Resources/CmdStrings.resx index 4df7b89b9d..8964becd37 100644 --- a/src/PSRule.EditorServices/Resources/CmdStrings.resx +++ b/src/PSRule.EditorServices/Resources/CmdStrings.resx @@ -192,4 +192,7 @@ Specifies a path to write results to. + + Do not restore modules before running rules. + \ No newline at end of file diff --git a/src/PSRule/Pipeline/CommandLineBuilder.cs b/src/PSRule/Pipeline/CommandLineBuilder.cs index c49b2c51f3..cf2d1fd975 100644 --- a/src/PSRule/Pipeline/CommandLineBuilder.cs +++ b/src/PSRule/Pipeline/CommandLineBuilder.cs @@ -26,7 +26,7 @@ public static class CommandLineBuilder /// A builder object to configure the pipeline. public static IInvokePipelineBuilder Invoke(string[] module, PSRuleOption option, IHostContext hostContext, LockFile? file = null) { - var sourcePipeline = new SourcePipelineBuilder(hostContext, option, GetLocalPath()); + var sourcePipeline = new SourcePipelineBuilder(hostContext, option, hostContext.CachePath ?? GetLocalPath()); LoadModules(sourcePipeline, module, option, file); var source = sourcePipeline.Build(); diff --git a/src/PSRule/Pipeline/HostContext.cs b/src/PSRule/Pipeline/HostContext.cs index d6b7dc7a2f..0e92131ad9 100644 --- a/src/PSRule/Pipeline/HostContext.cs +++ b/src/PSRule/Pipeline/HostContext.cs @@ -6,6 +6,8 @@ namespace PSRule.Pipeline; +#nullable enable + /// /// A base class for custom host context instances. /// @@ -94,4 +96,9 @@ public virtual string GetWorkingPath() { return Directory.GetCurrentDirectory(); } + + /// + public virtual string? CachePath => null; } + +#nullable restore diff --git a/src/PSRule/Pipeline/IHostContext.cs b/src/PSRule/Pipeline/IHostContext.cs index 44997fc0e5..761a0b3407 100644 --- a/src/PSRule/Pipeline/IHostContext.cs +++ b/src/PSRule/Pipeline/IHostContext.cs @@ -5,6 +5,8 @@ namespace PSRule.Pipeline; +#nullable enable + /// /// A host context for handling input and output emitted from the pipeline. /// @@ -76,4 +78,12 @@ public interface IHostContext /// Get the current working path. /// string GetWorkingPath(); + + /// + /// Configures the root path to use for caching artifacts including modules. + /// Each artifact is in a subdirectory of the root path. + /// + string? CachePath { get; } } + +#nullable restore diff --git a/src/PSRule/Pipeline/PSHostContext.cs b/src/PSRule/Pipeline/PSHostContext.cs index d38859c328..122b99aa23 100644 --- a/src/PSRule/Pipeline/PSHostContext.cs +++ b/src/PSRule/Pipeline/PSHostContext.cs @@ -5,6 +5,8 @@ namespace PSRule.Pipeline; +#nullable enable + /// /// The host context used for PowerShell-based pipelines. /// @@ -98,4 +100,9 @@ public string GetWorkingPath() { return ExecutionContext.SessionState.Path.CurrentFileSystemLocation.Path; } + + /// + public string? CachePath { get; } } + +#nullable restore diff --git a/src/PSRule/Pipeline/SourcePipelineBuilder.cs b/src/PSRule/Pipeline/SourcePipelineBuilder.cs index 04d7aee703..a3a9843985 100644 --- a/src/PSRule/Pipeline/SourcePipelineBuilder.cs +++ b/src/PSRule/Pipeline/SourcePipelineBuilder.cs @@ -30,18 +30,18 @@ public sealed class SourcePipelineBuilder : ISourcePipelineBuilder, ISourceComma private readonly IHostContext _HostContext; private readonly HostPipelineWriter _Writer; private readonly bool _UseDefaultPath; - private readonly string _LocalPath; + private readonly string _CachePath; private readonly RestrictScriptSource _RestrictScriptSource; private readonly string _WorkspacePath; - internal SourcePipelineBuilder(IHostContext hostContext, PSRuleOption option, string localPath = null) + internal SourcePipelineBuilder(IHostContext hostContext, PSRuleOption option, string cachePath = null) { _Source = new Dictionary(StringComparer.OrdinalIgnoreCase); _HostContext = hostContext; _Writer = new HostPipelineWriter(hostContext, option, ShouldProcess); _Writer.EnterScope("[Discovery.Source]"); _UseDefaultPath = option == null || option.Include == null || option.Include.Path == null; - _LocalPath = localPath; + _CachePath = cachePath; _RestrictScriptSource = option?.Execution?.RestrictScriptSource ?? ExecutionOption.Default.RestrictScriptSource.Value; _WorkspacePath = Environment.GetRootedBasePath(null); @@ -162,32 +162,29 @@ public void ModuleByName(string name, string version = null) private string FindModule(string name, string version) { - return TryPackagedModule(name, version, out var path) || + return TryPackagedModuleFromCache(name, version, out var path) || TryInstalledModule(name, version, out path) ? path : null; } /// /// Try to find a packaged module found relative to the tool. /// - private bool TryPackagedModule(string name, string version, out string path) + private bool TryPackagedModuleFromCache(string name, string version, out string path) { path = null; - if (_LocalPath == null) + if (_CachePath == null) return false; - Log($"[PSRule][S] -- Searching for module in: {_LocalPath}"); + Log($"[PSRule][S] -- Searching for module in: {_CachePath}"); if (!string.IsNullOrEmpty(version)) { - path = Environment.GetRootedBasePath(Path.Combine(_LocalPath, "Modules", name, version)); - if (System.IO.Directory.Exists(path)) + path = Environment.GetRootedBasePath(Path.Combine(_CachePath, "Modules", name, version)); + if (File.Exists(Path.Combine(path, GetManifestName(name)))) return true; } - path = Environment.GetRootedBasePath(Path.Combine(_LocalPath, "Modules", name)); - if (System.IO.Directory.Exists(path)) - return true; - - return System.IO.Directory.Exists(path); + path = Environment.GetRootedBasePath(Path.Combine(_CachePath, "Modules", name)); + return File.Exists(Path.Combine(path, GetManifestName(name))); } /// @@ -253,10 +250,10 @@ private static string[] SortModulePath(IEnumerable values) private Source.ModuleInfo LoadManifest(string basePath, string name) { var path = Path.Combine(basePath, GetManifestName(name)); + Log("[PSRule][S] -- Loading manifest from: {0}", path); if (!File.Exists(path)) return null; - Log("[PSRule][S] -- Loading manifest for: {0}", basePath); using var reader = new StreamReader(path); var data = reader.ReadToEnd(); var ast = System.Management.Automation.Language.Parser.ParseInput(data, out _, out _); diff --git a/src/vscode-ps-rule/README.md b/src/vscode-ps-rule/README.md index 5832288a0a..c83071157c 100644 --- a/src/vscode-ps-rule/README.md +++ b/src/vscode-ps-rule/README.md @@ -77,8 +77,10 @@ Name | Description `PSRule.execution.ruleSuppressed` | Determines how to handle suppressed rules. When set to `None`, PSRule will use the default (`Warn`), unless set by PSRule options. `PSRule.execution.unprocessedObject` | Determines how to report objects that are not processed by any rule. When set to `None`, PSRule will use the default (`Warn`), unless set by PSRule options. `PSRule.experimental.enabled` | Enables experimental features in the PSRule extension. -`PSRule.notifications.showChannelUpgrade` | Determines if a notification to switch to the stable channel is shown on start up. -`PSRule.notifications.showPowerShellExtension` | Determines if a notification to install the PowerShell extension is shown on start up. +`PSRule.lock.restore` | Determines if workspace modules will automatically be restored during activation. Modules can be restored manually using the `PSRule: Restore modules` command. +`PSRule.notifications.showChannelUpgrade` | Determines if a notification to switch to the stable channel is shown on activation. +`PSRule.notifications.showModuleRestore` | Determines if a notification to restore modules is shown on activation. +`PSRule.notifications.showPowerShellExtension` | Determines if a notification to install the PowerShell extension is shown on activation. `PSRule.options.path` | The path specifying a PSRule option file. When not set, the default `ps-rule.yaml` will be used from the current workspace. `PSRule.output.as` | Configures the output of analysis tasks, either summary or detailed. `PSRule.rule.baseline` | The name of the default baseline to use for executing rules. This setting can be overridden on individual PSRule tasks. diff --git a/src/vscode-ps-rule/commands/restore.ts b/src/vscode-ps-rule/commands/restore.ts index fa96704073..f610612f0d 100644 --- a/src/vscode-ps-rule/commands/restore.ts +++ b/src/vscode-ps-rule/commands/restore.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import * as cp from 'child_process'; -import { workspace } from 'vscode'; +import { workspace, window, ProgressLocation } from 'vscode'; import { getActiveOrFirstWorkspace } from '../utils'; import { logger } from '../logger'; import { ext } from '../extension'; @@ -18,30 +18,43 @@ export async function restore(): Promise { logger.log('Restoring modules.'); - const tool = cp.spawnSync(server.binPath, [server.languageServerPath, 'module', 'restore', '--verbose'], { - cwd: workspace.uri.fsPath, - }); + await window.withProgress({ + location: ProgressLocation.Window, + title: 'PSRule', + cancellable: false, + }, async (progress) => { + + progress.report({ message: 'Restoring modules' }); - tool.output?.forEach(o => { - - o?.toString().split('\n').forEach(line => { - if (line.startsWith('VERBOSE:')) { - logger.verbose(line); - } - else if (line.startsWith('ERROR:')) { - logger.error(line); - } - else { - logger.log(line); - } + const tool = cp.spawnSync(server.binPath, [server.languageServerPath, 'module', 'restore', '--verbose'], { + cwd: workspace.uri.fsPath, }); - }); - if (tool.status !== 0) { - logger.log(`Failed to restore modules. Exit code: ${tool.status}`); - return; - } - else { - logger.log('Modules restored.'); - } + progress.report({ message: 'Restoring modules' }); + + tool.output?.forEach(o => { + + o?.toString().split('\n').forEach(line => { + if (line.startsWith('VERBOSE:')) { + logger.verbose(line); + } + else if (line.startsWith('ERROR:')) { + logger.error(line); + } + else { + logger.log(line); + } + }); + }); + + if (tool.status !== 0) { + logger.log(`Failed to restore modules. Exit code: ${tool.status}`); + return; + } + else { + logger.log('Modules restored.'); + } + + progress.report({ message: 'Completed restore', increment: 100 }); + }); } diff --git a/src/vscode-ps-rule/configuration.ts b/src/vscode-ps-rule/configuration.ts index ea4c153886..b8f77cbc63 100644 --- a/src/vscode-ps-rule/configuration.ts +++ b/src/vscode-ps-rule/configuration.ts @@ -81,6 +81,11 @@ export interface ISetting { */ experimentalEnabled: boolean; + /** + * Determines if PSRule will automatically restore modules. + */ + lockRestore: boolean; + /** * The path specifying a PSRule option file. * When not set, the default ps-rule.yaml will be used from the current workspace. @@ -88,7 +93,20 @@ export interface ISetting { optionsPath: string | undefined; outputAs: OutputAs; + + /** + * Determines if a notification to switch to the stable channel is shown on activation. + */ notificationsShowChannelUpgrade: boolean; + + /** + * Determines if a notification to restore modules is shown on activation. + */ + notificationsShowModuleRestore: boolean; + + /** + * Determines if a notification to install the PowerShell extension is shown on activation. + */ notificationsShowPowerShellExtension: boolean; /** @@ -120,9 +138,11 @@ const globalDefaults: ISetting = { executionRuleSuppressed: ExecutionActionPreference.None, executionUnprocessedObject: ExecutionActionPreference.None, experimentalEnabled: false, + lockRestore: false, optionsPath: undefined, outputAs: OutputAs.Summary, notificationsShowChannelUpgrade: true, + notificationsShowModuleRestore: true, notificationsShowPowerShellExtension: true, ruleBaseline: undefined, // languageServerPath: undefined, @@ -206,6 +226,8 @@ export class ConfigurationManager { this.current.executionRuleSuppressed = config.get('execution.ruleSuppressed', this.default.executionRuleSuppressed); this.current.executionUnprocessedObject = config.get('execution.unprocessedObject', this.default.executionUnprocessedObject); + this.current.lockRestore = config.get('lock.restore') ?? this.default.lockRestore; + this.current.optionsPath = config.get('options.path') ?? this.default.optionsPath; this.current.outputAs = config.get('output.as', this.default.outputAs); @@ -215,6 +237,11 @@ export class ConfigurationManager { this.default.notificationsShowChannelUpgrade ); + this.current.notificationsShowModuleRestore = config.get( + 'notifications.showModuleRestore', + this.default.notificationsShowModuleRestore + ); + this.current.notificationsShowPowerShellExtension = config.get( 'notifications.showPowerShellExtension', this.default.notificationsShowPowerShellExtension diff --git a/src/vscode-ps-rule/extension.ts b/src/vscode-ps-rule/extension.ts index 545a595cc0..0e6b52ebff 100644 --- a/src/vscode-ps-rule/extension.ts +++ b/src/vscode-ps-rule/extension.ts @@ -4,10 +4,11 @@ 'use strict'; import * as path from 'path'; +import * as fse from 'fs-extra'; import * as vscode from 'vscode'; import { logger } from './logger'; import { PSRuleTaskProvider } from './tasks'; -import { ConfigurationManager } from './configuration'; +import { configuration, ConfigurationManager } from './configuration'; import { pwsh } from './powershell'; import { DocumentationLensProvider } from './docLens'; import { createOptionsFile } from './commands/createOptionsFile'; @@ -16,7 +17,7 @@ import { walkthroughCopySnippet } from './commands/walkthroughCopySnippet'; import { configureSettings } from './commands/configureSettings'; import { runAnalysisTask } from './commands/runAnalysisTask'; import { showTasks } from './commands/showTasks'; -import { PSRuleLanguageServer, getLanguageServer } from './utils'; +import { PSRuleLanguageServer, getActiveOrFirstWorkspace, getLanguageServer } from './utils'; import { restore } from './commands/restore'; export let taskManager: PSRuleTaskProvider | undefined; @@ -159,6 +160,8 @@ export class ExtensionManager implements vscode.Disposable { if (this.isTrusted) { this._server = await getLanguageServer(this._context); + + await this.restoreOnActivation() } } @@ -284,6 +287,62 @@ export class ExtensionManager implements vscode.Disposable { }; return result; } + + private async restoreOnActivation(): Promise { + const workspace = getActiveOrFirstWorkspace(); + if (!workspace) { + return; + } + + // Check if a lockfile exists. + const lockExists = await fse.exists(path.join(workspace.uri.fsPath, 'ps-rule.lock.json')); + if (!lockExists) { + return; + } + + // Check if the user has disabled the notification. + const shouldPrompt = configuration.get().notificationsShowModuleRestore + + // Check if restoring module is enabled. + let lockRestore = configuration.get().lockRestore + + // If restore is not enabled then prompt the user to enable it. + if (!lockRestore && shouldPrompt) { + const always = 'Always'; + const oneTime = 'One Time'; + const alwaysIgnore = 'Always Ignore'; + + await vscode.window + .showInformationMessage( + `Should PSRule automatically restore modules in your current workspace?`, + always, + oneTime, + alwaysIgnore + ) + .then((choice) => { + if (choice === always) { + vscode.workspace + .getConfiguration('PSRule.lock') + .update('restore', true, vscode.ConfigurationTarget.Global); + + lockRestore = true; + } + if (choice === oneTime) { + lockRestore = true; + } + if (choice === alwaysIgnore) { + vscode.workspace + .getConfiguration('PSRule.notifications') + .update('showModuleRestore', false, vscode.ConfigurationTarget.Global); + } + }); + } + + // Restore modules. + if (lockRestore) { + await restore(); + } + } } export const ext: ExtensionManager = new ExtensionManager(); diff --git a/src/vscode-ps-rule/runner.ts b/src/vscode-ps-rule/runner.ts new file mode 100644 index 0000000000..8a6ec5eeef --- /dev/null +++ b/src/vscode-ps-rule/runner.ts @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +'use strict'; + +import * as vscode from 'vscode'; +import { ISetting, ExecutionActionPreference, TraceLevelPreference } from './configuration'; +import { getActiveOrFirstWorkspace } from './utils'; + +export function getAnalysisRunner( + folder: vscode.WorkspaceFolder | undefined, + configuration: ISetting, + binPath: string, + languageServerPath: string, + path?: string, + inputPath?: string, + baseline?: string, + modules?: string[], + outcome?: string[], +): vscode.ProcessExecution { + const executionRuleExcluded = configuration.executionRuleExcluded; + const executionRuleSuppressed = configuration.executionRuleSuppressed; + const executionUnprocessedObject = configuration.executionUnprocessedObject; + const lockRestore = configuration.lockRestore; + const optionsPath = configuration.optionsPath; + const outputAs = configuration.outputAs; + const ruleBaseline = configuration.ruleBaseline; + const traceTask = configuration.traceTask; + + function getCmdTooling(): string[] { + let params: string[] = []; + + // Path + if (path !== undefined && path !== '') { + params.push('--path'); + params.push(`'${path}'`); + } + + // Options Path + if (optionsPath !== undefined && optionsPath !== '') { + params.push('--option'); + params.push(`'${optionsPath}'`); + } + + // Input Path + if (inputPath !== undefined && inputPath !== '') { + params.push('--input-path'); + params.push(inputPath); + } else { + params.push('--input-path'); + params.push('.'); + } + + // Baseline + if (baseline !== undefined && baseline !== '') { + params.push('--baseline'); + params.push(`'${baseline}'`); + } else if (ruleBaseline !== undefined && ruleBaseline !== '') { + params.push('--baseline'); + params.push(`'${ruleBaseline}'`); + } + + // Modules + if (modules !== undefined && modules.length > 0) { + for (let i = 0; i < modules.length; i++) { + params.push('--module'); + params.push(`'${modules[i]}'`); + } + } + + // Outcome + if (outcome !== undefined && outcome.length > 0) { + for (let i = 0; i < outcome.length; i++) { + params.push('--outcome'); + params.push(outcome[i]); + } + } + else { + params.push('--outcome'); + params.push('Fail'); + params.push('--outcome'); + params.push('Error'); + } + + // Toggle module restore + if (!lockRestore) { + params.push('--no-restore'); + } + + return params; + } + + // Set environment variables for the task. + let taskEnv: { [key: string]: string } = { + PSRULE_OUTPUT_STYLE: 'VisualStudioCode', + PSRULE_OUTPUT_AS: outputAs, + PSRULE_OUTPUT_CULTURE: vscode.env.language, + PSRULE_OUTPUT_BANNER: 'Minimal', + }; + + if (executionRuleExcluded !== undefined && executionRuleExcluded !== ExecutionActionPreference.None) { + taskEnv.PSRULE_EXECUTION_RULEEXCLUDED = executionRuleExcluded; + } + + if (executionRuleSuppressed !== undefined && executionRuleSuppressed !== ExecutionActionPreference.None) { + taskEnv.PSRULE_EXECUTION_RULESUPPRESSED = executionRuleSuppressed; + } + + if (executionUnprocessedObject !== undefined && executionUnprocessedObject !== ExecutionActionPreference.None) { + taskEnv.PSRULE_EXECUTION_UNPROCESSEDOBJECT = executionUnprocessedObject; + } + + const cwd = folder?.uri.fsPath ?? getActiveOrFirstWorkspace()?.uri.fsPath; + const args = [languageServerPath, 'run']; + args.push(...getCmdTooling()); + if (traceTask === TraceLevelPreference.Verbose) { + args.push('--verbose'); + } + + return new vscode.ProcessExecution( + binPath, + args, + { cwd: cwd, env: taskEnv }, + ) +} diff --git a/src/vscode-ps-rule/tasks.ts b/src/vscode-ps-rule/tasks.ts index cc8015fdc1..f96eb8b6a3 100644 --- a/src/vscode-ps-rule/tasks.ts +++ b/src/vscode-ps-rule/tasks.ts @@ -8,9 +8,9 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { defaultOptionsFile } from './consts'; import { ILogger, logger } from './logger'; -import { configuration, ExecutionActionPreference, TraceLevelPreference } from './configuration'; -import { getActiveOrFirstWorkspace } from './utils'; +import { configuration } from './configuration'; import { ext } from './extension'; +import { getAnalysisRunner } from './runner'; const emptyTasks: vscode.Task[] = []; @@ -219,76 +219,10 @@ export class PSRuleTaskProvider implements vscode.TaskProvider { }; } - const executionRuleExcluded = configuration.get().executionRuleExcluded; - const executionRuleSuppressed = configuration.get().executionRuleSuppressed; - const executionUnprocessedObject = configuration.get().executionUnprocessedObject; - const optionsPath = configuration.get().optionsPath; - const outputAs = configuration.get().outputAs; - const ruleBaseline = configuration.get().ruleBaseline; - const traceTask = configuration.get().traceTask; - function getTaskName() { return name; } - function getCmdTooling(): string[] { - let params: string[] = []; - - // Path - if (path !== undefined && path !== '') { - params.push('--path'); - params.push(`'${path}'`); - } - - // Options Path - if (optionsPath !== undefined && optionsPath !== '') { - params.push('--option'); - params.push(`'${optionsPath}'`); - } - - // Input Path - if (inputPath !== undefined && inputPath !== '') { - params.push('--input-path'); - params.push(inputPath); - } else { - params.push('--input-path'); - params.push('.'); - } - - // Baseline - if (baseline !== undefined && baseline !== '') { - params.push('--baseline'); - params.push(`'${baseline}'`); - } else if (ruleBaseline !== undefined && ruleBaseline !== '') { - params.push('--baseline'); - params.push(`'${ruleBaseline}'`); - } - - // Modules - if (modules !== undefined && modules.length > 0) { - for (let i = 0; i < modules.length; i++) { - params.push('--module'); - params.push(`'${modules[i]}'`); - } - } - - // Outcome - if (outcome !== undefined && outcome.length > 0) { - for (let i = 0; i < outcome.length; i++) { - params.push('--outcome'); - params.push(outcome[i]); - } - } - else { - params.push('--outcome'); - params.push('Fail'); - params.push('--outcome'); - params.push('Error'); - } - - return params; - } - const taskName = getTaskName(); const binPath = ext.server?.binPath; const languageServerPath = ext.server?.languageServerPath @@ -307,32 +241,7 @@ export class PSRuleTaskProvider implements vscode.TaskProvider { ); } - // Set environment variables for the task. - let taskEnv: { [key: string]: string } = { - PSRULE_OUTPUT_STYLE: 'VisualStudioCode', - PSRULE_OUTPUT_AS: outputAs, - PSRULE_OUTPUT_CULTURE: vscode.env.language, - PSRULE_OUTPUT_BANNER: 'Minimal', - }; - - if (executionRuleExcluded !== undefined && executionRuleExcluded !== ExecutionActionPreference.None) { - taskEnv.PSRULE_EXECUTION_RULEEXCLUDED = executionRuleExcluded; - } - - if (executionRuleSuppressed !== undefined && executionRuleSuppressed !== ExecutionActionPreference.None) { - taskEnv.PSRULE_EXECUTION_RULESUPPRESSED = executionRuleSuppressed; - } - - if (executionUnprocessedObject !== undefined && executionUnprocessedObject !== ExecutionActionPreference.None) { - taskEnv.PSRULE_EXECUTION_UNPROCESSEDOBJECT = executionUnprocessedObject; - } - - const cwd = folder?.uri.fsPath ?? getActiveOrFirstWorkspace()?.uri.fsPath; - const args = [languageServerPath, 'run']; - args.push(...getCmdTooling()); - if (traceTask === TraceLevelPreference.Verbose) { - args.push('--verbose'); - } + const runner = getAnalysisRunner(folder, configuration.get(), binPath, languageServerPath, path, inputPath, baseline, modules, outcome); // Return the task instance. const t = new vscode.Task( @@ -340,11 +249,7 @@ export class PSRuleTaskProvider implements vscode.TaskProvider { folder ?? vscode.TaskScope.Workspace, taskName, PSRuleTaskProvider.taskType, - new vscode.ProcessExecution( - binPath, - args, - { cwd: cwd, env: taskEnv }, - ), + runner, matcher, ); t.detail = 'Run analysis for current workspace.'; @@ -352,7 +257,7 @@ export class PSRuleTaskProvider implements vscode.TaskProvider { echo: false, }; - const parameterArgs = args.slice(1); + const parameterArgs = runner.args.slice(1); logger.verbose(`Preparing task '${taskName}' with arguments: ${parameterArgs.join(' ')}`); return t; } diff --git a/src/vscode-ps-rule/test/suite/configuration.test.ts b/src/vscode-ps-rule/test/suite/configuration.test.ts index 570259f879..a123127e05 100644 --- a/src/vscode-ps-rule/test/suite/configuration.test.ts +++ b/src/vscode-ps-rule/test/suite/configuration.test.ts @@ -17,8 +17,10 @@ suite('ConfigurationManager tests', () => { assert.equal(config.get().executionRuleSuppressed, ExecutionActionPreference.None); assert.equal(config.get().executionUnprocessedObject, ExecutionActionPreference.None); assert.equal(config.get().experimentalEnabled, false); + assert.equal(config.get().lockRestore, false); assert.equal(config.get().outputAs, OutputAs.Summary); assert.equal(config.get().notificationsShowChannelUpgrade, true); + assert.equal(config.get().notificationsShowModuleRestore, true); assert.equal(config.get().notificationsShowPowerShellExtension, true); assert.equal(config.get().ruleBaseline, undefined); //assert.equal(config.get().languageServerPath, false);