Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial language server implementation #2711

Merged
merged 3 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions PSRule.sln
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Types.Tests", "tests
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSRule.EditorServices", "src\PSRule.EditorServices\PSRule.EditorServices.csproj", "{061DD38A-B9E9-4EF1-B5B7-D0A484DB74D1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSRule.EditorServices.Tests", "tests\PSRule.EditorServices.Tests\PSRule.EditorServices.Tests.csproj", "{89D6AAAA-2AD9-4899-935E-5330EFB3383E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -100,6 +102,10 @@ Global
{061DD38A-B9E9-4EF1-B5B7-D0A484DB74D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{061DD38A-B9E9-4EF1-B5B7-D0A484DB74D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{061DD38A-B9E9-4EF1-B5B7-D0A484DB74D1}.Release|Any CPU.Build.0 = Release|Any CPU
{89D6AAAA-2AD9-4899-935E-5330EFB3383E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{89D6AAAA-2AD9-4899-935E-5330EFB3383E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{89D6AAAA-2AD9-4899-935E-5330EFB3383E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{89D6AAAA-2AD9-4899-935E-5330EFB3383E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -109,6 +115,7 @@ 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}
{89D6AAAA-2AD9-4899-935E-5330EFB3383E} = {E0EA0CBA-96C5-4447-8B69-BC13EF0D7A4A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {533491EB-BAE9-472E-B57F-A675ECD335B5}
Expand Down
3 changes: 3 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,6 @@ The following table lists exit codes that may be returned by the PSRule language
Exit code | Description | Notes
--------- | ----------- | -----
0 | Success | The language server exited during normal operation.
901 | The language server was unable to start due to missing or invalid configuration. | Unexpected. Please report this issue.
902 | The language server encountered an unexpected exception and stopped. | Unexpected. Please report this issue.
903 | A debugger failed to attach to the language server. | When debugging the language server, ensure the debugger is attached within 5 minutes.
24 changes: 13 additions & 11 deletions src/PSRule.CommandLine/Commands/ModuleCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ namespace PSRule.CommandLine.Commands;
/// </summary>
public sealed class ModuleCommand
{
private const int ERROR_SUCCESS = 0;

/// <summary>
/// Failed to install a module.
/// </summary>
Expand All @@ -46,7 +48,7 @@ public sealed class ModuleCommand
/// </summary>
public static async Task<int> ModuleRestoreAsync(RestoreOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
{
var exitCode = 0;
var exitCode = ERROR_SUCCESS;
var requires = clientContext.Option.Requires.ToDictionary();
var file = LockFile.Read(null);

Expand Down Expand Up @@ -135,7 +137,7 @@ public static async Task<int> ModuleRestoreAsync(RestoreOptions operationOptions
}
}

if (exitCode == 0)
if (exitCode == ERROR_SUCCESS)
{
clientContext.LogVerbose("[PSRule][M] -- All modules are restored and up-to-date.");
}
Expand All @@ -152,7 +154,7 @@ public static async Task<int> ModuleRestoreAsync(RestoreOptions operationOptions
/// </summary>
public static async Task<int> ModuleInitAsync(ModuleOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
{
var exitCode = 0;
var exitCode = ERROR_SUCCESS;
var requires = clientContext.Option.Requires.ToDictionary();
var existingFile = LockFile.Read(null);
var file = !operationOptions.Force ? existingFile : new LockFile();
Expand Down Expand Up @@ -218,12 +220,12 @@ public static async Task<int> ModuleInitAsync(ModuleOptions operationOptions, Cl
public static async Task<int> ModuleListAsync(ModuleOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
var exitCode = 0;
var exitCode = ERROR_SUCCESS;
var requires = clientContext.Option.Requires.ToDictionary();
var file = LockFile.Read(null);
var pwsh = CreatePowerShell();

if (exitCode == 0)
if (exitCode == ERROR_SUCCESS)
{
ListModules(clientContext, GetModules(pwsh, file, clientContext.Option));
}
Expand All @@ -235,7 +237,7 @@ public static async Task<int> ModuleListAsync(ModuleOptions operationOptions, Cl
/// </summary>
public static async Task<int> ModuleAddAsync(ModuleOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
{
var exitCode = 0;
var exitCode = ERROR_SUCCESS;
if (operationOptions.Module == null || operationOptions.Module.Length == 0) return exitCode;

var requires = clientContext.Option.Requires.ToDictionary();
Expand Down Expand Up @@ -296,7 +298,7 @@ public static async Task<int> ModuleAddAsync(ModuleOptions operationOptions, Cli

file.Write(null);

if (exitCode == 0)
if (exitCode == ERROR_SUCCESS)
{
ListModules(clientContext, GetModules(pwsh, file, clientContext.Option));
}
Expand All @@ -310,7 +312,7 @@ public static async Task<int> ModuleAddAsync(ModuleOptions operationOptions, Cli
public static async Task<int> ModuleRemoveAsync(ModuleOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
var exitCode = 0;
var exitCode = ERROR_SUCCESS;
if (operationOptions.Module == null || operationOptions.Module.Length == 0) return exitCode;

var file = LockFile.Read(null);
Expand All @@ -330,7 +332,7 @@ public static async Task<int> ModuleRemoveAsync(ModuleOptions operationOptions,

file.Write(null);

if (exitCode == 0)
if (exitCode == ERROR_SUCCESS)
{
ListModules(clientContext, GetModules(pwsh, file, clientContext.Option));
}
Expand All @@ -342,7 +344,7 @@ public static async Task<int> ModuleRemoveAsync(ModuleOptions operationOptions,
/// </summary>
public static async Task<int> ModuleUpgradeAsync(ModuleOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
{
var exitCode = 0;
var exitCode = ERROR_SUCCESS;
var requires = clientContext.Option.Requires.ToDictionary();
var file = LockFile.Read(null);
var filteredModules = operationOptions.Module != null && operationOptions.Module.Length > 0 ? new HashSet<string>(operationOptions.Module, StringComparer.OrdinalIgnoreCase) : null;
Expand Down Expand Up @@ -379,7 +381,7 @@ public static async Task<int> ModuleUpgradeAsync(ModuleOptions operationOptions,

file.Write(null);

if (exitCode == 0)
if (exitCode == ERROR_SUCCESS)
{
ListModules(clientContext, GetModules(pwsh, file, clientContext.Option));
}
Expand Down
2 changes: 1 addition & 1 deletion src/PSRule.CommandLine/Models/ModuleOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace PSRule.CommandLine.Models;

/// <summary>
/// Options for the module command.
/// Options for the <c>module</c> command.
/// </summary>
public sealed class ModuleOptions
{
Expand Down
36 changes: 36 additions & 0 deletions src/PSRule.EditorServices/ClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using PSRule.CommandLine;
using PSRule.CommandLine.Commands;
using PSRule.CommandLine.Models;
using PSRule.EditorServices.Commands;
using PSRule.EditorServices.Models;
using PSRule.EditorServices.Resources;
using PSRule.Pipeline;

Expand Down Expand Up @@ -37,6 +39,8 @@ internal sealed class ClientBuilder
private readonly Option<string> _Run_Baseline;
private readonly Option<string[]> _Run_Outcome;
private readonly Option<bool> _Run_NoRestore;
private readonly Option<bool> _Listen_Stdio;
private readonly Option<string> _Listen_Pipe;

private ClientBuilder(RootCommand cmd)
{
Expand Down Expand Up @@ -115,6 +119,17 @@ private ClientBuilder(RootCommand cmd)
description: CmdStrings.Module_Restore_Force_Description
);

// Options for the listen command.
_Listen_Stdio = new Option<bool>(
["--stdio"],
description: CmdStrings.Listen_Stdio_Description
);

_Listen_Pipe = new Option<string>(
["--pipe"],
description: CmdStrings.Listen_Pipe_Description
);

cmd.AddGlobalOption(_Global_Option);
cmd.AddGlobalOption(_Global_Verbose);
cmd.AddGlobalOption(_Global_Debug);
Expand All @@ -132,6 +147,7 @@ public static Command New()
var builder = new ClientBuilder(cmd);
builder.AddRun();
builder.AddModule();
builder.AddListen();
return builder.Command;
}

Expand Down Expand Up @@ -322,6 +338,26 @@ private void AddModule()
Command.AddCommand(cmd);
}

private void AddListen()
{
var cmd = new Command("listen", CmdStrings.Listen_Description);
cmd.AddOption(_Global_Path);
cmd.AddOption(_Listen_Stdio);
cmd.AddOption(_Listen_Pipe);
cmd.SetHandler(async (invocation) =>
{
var option = new ListenOptions
{
Path = invocation.ParseResult.GetValueForOption(_Global_Path),
Pipe = invocation.ParseResult.GetValueForOption(_Listen_Pipe),
Stdio = invocation.ParseResult.GetValueForOption(_Listen_Stdio),
};
var client = GetClientContext(invocation);
invocation.ExitCode = await ListenCommand.ListenAsync(option, client);
});
Command.AddCommand(cmd);
}

private ClientContext GetClientContext(InvocationContext invocation)
{
var option = invocation.ParseResult.GetValueForOption(_Global_Option).TrimQuotes();
Expand Down
119 changes: 119 additions & 0 deletions src/PSRule.EditorServices/Commands/ListenCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.IO.Pipes;
using PSRule.CommandLine;
using PSRule.EditorServices.Hosting;
using PSRule.EditorServices.Models;

namespace PSRule.EditorServices.Commands;

/// <summary>
/// The listen command for running the persistent language server.
/// </summary>
internal sealed class ListenCommand
{
private const string LOCAL_PIPE_SERVER_NAME = ".";
private const string WINDOWS_PIPE_PREFIX = @"\\.\pipe\";

private const int ERROR_SUCCESS = 0;
private const int ERROR_INVALID_CONFIGURATION = 901;
private const int ERROR_SERVER_EXCEPTION = 902;
private const int ERROR_DEBUGGER_ATTACH_TIMEOUT = 903;

/// <summary>
/// Run the listen command.
/// This command will start a language server and block until exit.
/// </summary>
public static async Task<int> ListenAsync(ListenOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
{
if (operationOptions.WaitForDebugger)
{
if (!await WaitForDebuggerAsync(cancellationToken))
return ERROR_DEBUGGER_ATTACH_TIMEOUT;

System.Diagnostics.Debugger.Break();
}

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

try
{
await server.RunAsync(cancellationToken);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return ERROR_SERVER_EXCEPTION;
}

return ERROR_SUCCESS;
}

private static async Task<LspServer?> GetServerAsync(ListenOptions operationOptions, CancellationToken cancellationToken)
{
// Create a server with a named pipe client stream.
if (operationOptions.Pipe is { } pipeName)
{
var clientPipe = await ConnectNamedPipeClientStreamAsync(pipeName, cancellationToken);

return new LspServer(options =>
{
options.WithInput(clientPipe)
.WithOutput(clientPipe)
.RegisterForDisposal(clientPipe);
});
}
else if (operationOptions.Stdio)
{
return new LspServer(options =>
{
options.WithInput(Console.OpenStandardInput())
.WithOutput(Console.OpenStandardOutput());
});
}

return null;
}

/// <summary>
/// Connect to a named pipe client stream.
/// </summary>
private static async Task<NamedPipeClientStream> ConnectNamedPipeClientStreamAsync(string pipeName, CancellationToken cancellationToken)
{
pipeName = pipeName.StartsWith(WINDOWS_PIPE_PREFIX) ? pipeName[WINDOWS_PIPE_PREFIX.Length..] : pipeName;

var clientPipe = new NamedPipeClientStream(LOCAL_PIPE_SERVER_NAME, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
await clientPipe.ConnectAsync(cancellationToken);

return clientPipe;
}

/// <summary>
/// Wait up to 5 minutes for the debugger to attach.
/// </summary>
private static async Task<bool> WaitForDebuggerAsync(CancellationToken cancellationToken)
{
try
{
var debuggerTimeoutToken = CancellationTokenSource.CreateLinkedTokenSource
(
cancellationToken,
new CancellationTokenSource(TimeSpan.FromMinutes(5)).Token
).Token;

while (!System.Diagnostics.Debugger.IsAttached)
{
await Task.Delay(500, debuggerTimeoutToken);
}
}
catch
{
return false;
}

return true;
}
}
24 changes: 24 additions & 0 deletions src/PSRule.EditorServices/Hosting/ILanguageServerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Newtonsoft.Json.Linq;
using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Server;

namespace PSRule.EditorServices.Hosting;

/// <summary>
/// Extension methods for working with <see cref="ILanguageServer"/>.
/// </summary>
internal static class ILanguageServerExtensions
{
public static void SendServerReady(this ILanguageServer server)
{
server.SendProgress(new ProgressParams
{
Token = new ProgressToken("server/ready"),
Value = new JValue(Engine.GetVersion()),
});
}
}
Loading
Loading