Skip to content

Commit

Permalink
Added support dependency management in VSCode #2734
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite committed Jan 25, 2025
1 parent 2217210 commit 1354620
Show file tree
Hide file tree
Showing 23 changed files with 978 additions and 304 deletions.
5 changes: 5 additions & 0 deletions docs/CHANGELOG-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers

What's changed since pre-release v3.0.0-B0390:

- New features:
- Added support dependency management in VSCode by @BernieWhite.
[#2734](https://github.com/microsoft/PSRule/issues/2734)
- Code lens in `ps-rule.lock.json` allows you to upgrade all or specific modules to the latest version.
- The command `Upgrade dependency` allows you to upgrade all or specific modules to the latest version.
- Bug fixes:
- Fixed upgrade dependency could use pre-release version by @BernieWhite.
[#2726](https://github.com/microsoft/PSRule/issues/2726)
Expand Down
33 changes: 21 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@
"command": "PSRule.initLock",
"title": "Initialize lock file",
"category": "PSRule"
},
{
"command": "PSRule.upgradeDependency",
"title": "Upgrade dependency",
"category": "PSRule"
}
],
"configuration": [
Expand All @@ -115,7 +120,13 @@
"PSRule.codeLens.ruleDocumentationLinks": {
"type": "boolean",
"default": true,
"description": "Enables Code Lens that displays links to rule documentation.",
"markdownDescription": "Enables Code Lens that displays links to rule documentation.",
"scope": "application"
},
"PSRule.codeLens.dependencyManagement": {
"type": "boolean",
"default": true,
"markdownDescription": "Enables Code Lens that displays links to manage dependencies.",
"scope": "application"
},
"PSRule.documentation.path": {
Expand All @@ -127,13 +138,13 @@
"PSRule.documentation.localePath": {
"type": "string",
"default": null,
"description": "The locale path to use for locating rule documentation. The VS Code locale will be used by default.",
"markdownDescription": "The locale path to use for locating rule documentation. The VS Code locale will be used by default.",
"scope": "window"
},
"PSRule.documentation.customSnippetPath": {
"type": "string",
"default": null,
"description": "The path to a file containing a rule documentation snippet. When not set, built-in PSRule snippets will be used.",
"markdownDescription": "The path to a file containing a rule documentation snippet. When not set, built-in PSRule snippets will be used.",
"scope": "window"
},
"PSRule.documentation.snippet": {
Expand Down Expand Up @@ -199,45 +210,43 @@
"PSRule.experimental.enabled": {
"type": "boolean",
"default": false,
"description": "Enables experimental features in the PSRule extension.",
"markdownDescription": "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 activation.",
"markdownDescription": "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.",
"markdownDescription": "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 activation.",
"markdownDescription": "Determines if a notification to install the PowerShell extension is shown on activation.",
"scope": "application"
},
"PSRule.options.path": {
"type": "string",
"default": null,
"description": "The path specifying a PSRule option file. When not set, the default ps-rule.yaml will be used from the current workspace.",
"markdownDescription": "The path specifying a PSRule option file. When not set, the default `ps-rule.yaml` will be used from the current workspace.",
"scope": "window"
},
"PSRule.output.as": {
"type": "string",
"default": "Summary",
"description": "Configures the output of analysis tasks, either summary or detailed.",
"markdownDescription": "Configures the output of analysis tasks, either summary or detailed.",
"enum": [
"Detail",
"Summary"
Expand All @@ -247,13 +256,13 @@
"PSRule.rule.baseline": {
"type": "string",
"default": null,
"description": "The name of the default baseline to use for executing rules. This setting can be overridden on individual PSRule tasks.",
"markdownDescription": "The name of the default baseline to use for executing rules. This setting can be overridden on individual PSRule tasks.",
"scope": "window"
},
"PSRule.trace.task": {
"type": "string",
"default": "Off",
"description": "Determines if verbose logging is enabled for task output.",
"markdownDescription": "Determines if verbose logging is enabled for task output.",
"enum": [
"Off",
"Verbose"
Expand Down
7 changes: 4 additions & 3 deletions src/PSRule.CommandLine/Commands/ModuleCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.Collections;
using System.CommandLine.IO;
using System.Diagnostics.CodeAnalysis;
using System.Management.Automation;
using Newtonsoft.Json;
Expand Down Expand Up @@ -376,7 +377,7 @@ public static async Task<int> ModuleUpgradeAsync(ModuleOptions operationOptions,
if (idealVersion == kv.Value.Version)
continue;

if (!IsInstalled(pwsh, kv.Key, idealVersion, out _, out _) && await InstallVersionAsync(clientContext, kv.Key, idealVersion, null, cancellationToken) == null)
if (!IsInstalled(pwsh, kv.Key, idealVersion, out _, out var modulePath) && await InstallVersionAsync(clientContext, kv.Key, idealVersion, null, cancellationToken) == null)
{
clientContext.LogError(Messages.Error_501, kv.Key, idealVersion);
return ERROR_MODULE_FAILED_TO_INSTALL;
Expand All @@ -385,7 +386,7 @@ public static async Task<int> ModuleUpgradeAsync(ModuleOptions operationOptions,
clientContext.LogVerbose(Messages.UsingModule, kv.Key, idealVersion.ToString());

kv.Value.Version = idealVersion;
kv.Value.Integrity = IntegrityBuilder.Build(clientContext.IntegrityAlgorithm, GetModulePath(clientContext, kv.Key, idealVersion));
kv.Value.Integrity = IntegrityBuilder.Build(clientContext.IntegrityAlgorithm, modulePath ?? GetModulePath(clientContext, kv.Key, idealVersion));
kv.Value.IncludePrerelease = (kv.Value.IncludePrerelease.GetValueOrDefault(false) || operationOptions.Prerelease) && !idealVersion.Stable ? true : null;
file.Modules[kv.Key] = kv.Value;
}
Expand Down Expand Up @@ -441,7 +442,7 @@ private static IEnumerable<ModuleRecord> GetModules(PowerShell pwsh, LockFile fi

private static void ListModules(ClientContext context, IEnumerable<ModuleRecord> results)
{
context.Invocation.Console.Out.Write(JsonConvert.SerializeObject(results, new JsonSerializerSettings
context.Invocation.Console.Out.WriteLine(JsonConvert.SerializeObject(results, new JsonSerializerSettings
{
Formatting = Formatting.Indented
}));
Expand Down
33 changes: 28 additions & 5 deletions src/PSRule.EditorServices/Commands/ListenCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the MIT License.

using System.IO.Pipes;
using Microsoft.Extensions.DependencyInjection;
using OmniSharp.Extensions.LanguageServer.Server;
using PSRule.CommandLine;
using PSRule.EditorServices.Hosting;
using PSRule.EditorServices.Models;
Expand All @@ -16,6 +18,12 @@ internal sealed class ListenCommand
private const string LOCAL_PIPE_SERVER_NAME = ".";
private const string WINDOWS_PIPE_PREFIX = @"\\.\pipe\";

// Timeout in minutes to wait for the debugger to attach.
private const int DEBUGGER_ATTACH_TIMEOUT = 5;

// Time in milliseconds to wait between debugger attach checks.
private const int DEBUGGER_ATTACH_CYCLE_WAIT_TIME = 500;

private const int ERROR_SUCCESS = 0;
private const int ERROR_INVALID_CONFIGURATION = 901;
private const int ERROR_SERVER_EXCEPTION = 902;
Expand All @@ -35,13 +43,13 @@ public static async Task<int> ListenAsync(ListenOptions operationOptions, Client
System.Diagnostics.Debugger.Break();
}

var server = await GetServerAsync(operationOptions, cancellationToken);
var server = await GetServerAsync(operationOptions, clientContext, cancellationToken);
if (server == null)
return ERROR_INVALID_CONFIGURATION;

try
{
await server.RunAsync(cancellationToken);
await server.RunWaitAsync(cancellationToken);
}
catch (Exception ex)
{
Expand All @@ -52,7 +60,7 @@ public static async Task<int> ListenAsync(ListenOptions operationOptions, Client
return ERROR_SUCCESS;
}

private static async Task<LspServer?> GetServerAsync(ListenOptions operationOptions, CancellationToken cancellationToken)
private static async Task<LspServer?> GetServerAsync(ListenOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken)
{
// Create a server with a named pipe client stream.
if (operationOptions.Pipe is { } pipeName)
Expand All @@ -61,6 +69,8 @@ public static async Task<int> ListenAsync(ListenOptions operationOptions, Client

return new LspServer(options =>
{
WithServices(options, clientContext);

options.WithInput(clientPipe)
.WithOutput(clientPipe)
.RegisterForDisposal(clientPipe);
Expand All @@ -70,6 +80,8 @@ public static async Task<int> ListenAsync(ListenOptions operationOptions, Client
{
return new LspServer(options =>
{
WithServices(options, clientContext);

options.WithInput(Console.OpenStandardInput())
.WithOutput(Console.OpenStandardOutput());
});
Expand Down Expand Up @@ -101,12 +113,12 @@ private static async Task<bool> WaitForDebuggerAsync(CancellationToken cancellat
var debuggerTimeoutToken = CancellationTokenSource.CreateLinkedTokenSource
(
cancellationToken,
new CancellationTokenSource(TimeSpan.FromMinutes(5)).Token
new CancellationTokenSource(TimeSpan.FromMinutes(DEBUGGER_ATTACH_TIMEOUT)).Token
).Token;

while (!System.Diagnostics.Debugger.IsAttached)
{
await Task.Delay(500, debuggerTimeoutToken);
await Task.Delay(DEBUGGER_ATTACH_CYCLE_WAIT_TIME, debuggerTimeoutToken);
}
}
catch
Expand All @@ -116,4 +128,15 @@ private static async Task<bool> WaitForDebuggerAsync(CancellationToken cancellat

return true;
}

/// <summary>
/// Register services for dependency injection.
/// </summary>
private static void WithServices(LanguageServerOptions options, ClientContext clientContext)
{
options.WithServices(services =>
{
services.AddSingleton<ClientContext>(clientContext);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using OmniSharp.Extensions.JsonRpc;
using OmniSharp.Extensions.LanguageServer.Protocol.Window;
using OmniSharp.Extensions.LanguageServer.Protocol.Workspace;
using PSRule.CommandLine;
using PSRule.CommandLine.Commands;
using PSRule.CommandLine.Models;
using PSRule.Runtime;

namespace PSRule.EditorServices.Handlers;

/// <summary>
/// Handler the upgradeDependency command.
/// This command upgrades a module dependency to the latest version that meeds any specified constraints.
/// </summary>
public sealed class UpgradeDependencyCommandHandler(ClientContext context, ISerializer serializer, ILogger logger)
: ExecuteTypedResponseCommandHandlerBase<UpgradeDependencyCommandHandlerInput, UpgradeDependencyCommandHandlerOutput>(COMMAND_NAME, serializer)
{
private const string COMMAND_NAME = "upgradeDependency";
private const string ALL_MODULES_PLACEHOLDER = "*";

private readonly ClientContext _Context = context;
private readonly ILogger _Logger = logger;

/// <summary>
/// Handle the upgradeDependency command.
/// </summary>
public override async Task<UpgradeDependencyCommandHandlerOutput> Handle(UpgradeDependencyCommandHandlerInput input, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(input, nameof(input));
ArgumentException.ThrowIfNullOrWhiteSpace(input.Path, nameof(input.Path));
ArgumentException.ThrowIfNullOrWhiteSpace(input.Module, nameof(input.Module));

var all = input.Module == ALL_MODULES_PLACEHOLDER;

var options = new ModuleOptions
{
Module = all ? null : [input.Module],
};

if (all)
{
_Logger.LogInformation(new EventId(0), "Checking for upgrades of all modules in: {0}", input.Path);
}
else
{
_Logger.LogInformation(new EventId(0), "Checking for upgrades of module {0} in: {1}", input.Module, input.Path);
}

try
{
var exitCode = await ModuleCommand.ModuleUpgradeAsync(options, _Context, cancellationToken);
if (exitCode != 0)
{
throw new InvalidOperationException("Module upgrade failed");
}
}
catch (Exception ex)
{
_Logger.LogError(new EventId(0), ex, ex.Message);
}

return new UpgradeDependencyCommandHandlerOutput();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.EditorServices.Handlers;

/// <summary>
/// Input for the upgrade dependency command handler.
/// </summary>
public sealed class UpgradeDependencyCommandHandlerInput
{
/// <summary>
/// The path to the options file.
/// </summary>
public string? Path { get; set; }

/// <summary>
/// The module to upgrade.
/// </summary>
public string? Module { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.EditorServices.Handlers;

/// <summary>
/// Output for the upgrade dependency command handler.
/// </summary>
public sealed class UpgradeDependencyCommandHandlerOutput
{

}
Loading

0 comments on commit 1354620

Please sign in to comment.