|
| 1 | +// Copyright (c) Microsoft Corporation. |
| 2 | +// Licensed under the MIT License. |
| 3 | +#nullable enable |
| 4 | + |
| 5 | +using System; |
| 6 | +using System.Collections.Generic; |
| 7 | +using System.Diagnostics; |
| 8 | +using System.IO; |
| 9 | +using System.Threading; |
| 10 | +using System.Threading.Tasks; |
| 11 | + |
| 12 | +namespace PowerShellEditorServices.Test.E2E; |
| 13 | + |
| 14 | +/// <summary> |
| 15 | +/// Hosts a language server process that communicates over stdio |
| 16 | +/// </summary> |
| 17 | +internal class StdioLanguageServerProcessHost(string fileName, IEnumerable<string> argumentList) : IAsyncLanguageServerHost |
| 18 | +{ |
| 19 | + // The PSES process that will be started and managed |
| 20 | + private readonly Process process = new() |
| 21 | + { |
| 22 | + EnableRaisingEvents = true, |
| 23 | + StartInfo = new ProcessStartInfo(fileName, argumentList) |
| 24 | + { |
| 25 | + RedirectStandardInput = true, |
| 26 | + RedirectStandardOutput = true, |
| 27 | + RedirectStandardError = true, |
| 28 | + UseShellExecute = false, |
| 29 | + CreateNoWindow = true |
| 30 | + } |
| 31 | + }; |
| 32 | + |
| 33 | + // Track the state of the startup |
| 34 | + private TaskCompletionSource<(StreamReader, StreamWriter)>? startTcs; |
| 35 | + private TaskCompletionSource<bool>? stopTcs; |
| 36 | + |
| 37 | + // Starts the process. Returns when the process has started and streams are available. |
| 38 | + public async Task<(StreamReader, StreamWriter)> Start(CancellationToken token = default) |
| 39 | + { |
| 40 | + // Runs this once upon process exit to clean up the state. |
| 41 | + EventHandler? exitHandler = null; |
| 42 | + exitHandler = (sender, e) => |
| 43 | + { |
| 44 | + // Complete the stopTcs task when the process finally exits, allowing stop to complete |
| 45 | + stopTcs?.TrySetResult(true); |
| 46 | + stopTcs = null; |
| 47 | + startTcs = null; |
| 48 | + process.Exited -= exitHandler; |
| 49 | + }; |
| 50 | + process.Exited += exitHandler; |
| 51 | + |
| 52 | + if (stopTcs is not null) |
| 53 | + { |
| 54 | + throw new InvalidOperationException("The process is currently stopping and cannot be started."); |
| 55 | + } |
| 56 | + |
| 57 | + // Await the existing task if we have already started, making this operation idempotent |
| 58 | + if (startTcs is not null) |
| 59 | + { |
| 60 | + return await startTcs.Task; |
| 61 | + } |
| 62 | + |
| 63 | + // Initiate a new startTcs to track the startup |
| 64 | + startTcs = new(); |
| 65 | + |
| 66 | + token.ThrowIfCancellationRequested(); |
| 67 | + |
| 68 | + // Should throw if there are any startup problems such as invalid path, etc. |
| 69 | + process.Start(); |
| 70 | + |
| 71 | + // According to the source the streams should be allocated synchronously after the process has started, however it's not super clear so we will put this here in case there is an explicit race condition. |
| 72 | + if (process.StandardInput.BaseStream is null || process.StandardOutput.BaseStream is null) |
| 73 | + { |
| 74 | + throw new InvalidOperationException("The process has started but the StandardInput or StandardOutput streams are not available. This should never happen and is probably a race condition, please report it to PowerShellEditorServices."); |
| 75 | + } |
| 76 | + |
| 77 | + startTcs.SetResult(( |
| 78 | + process.StandardOutput, |
| 79 | + process.StandardInput |
| 80 | + )); |
| 81 | + |
| 82 | + // Return the result of the completion task |
| 83 | + return await startTcs.Task; |
| 84 | + } |
| 85 | + |
| 86 | + public async Task WaitForExit(CancellationToken token = default) |
| 87 | + { |
| 88 | + AssertStarting(); |
| 89 | + await process.WaitForExitAsync(token); |
| 90 | + } |
| 91 | + |
| 92 | + /// <summary> |
| 93 | + /// Determines if the process is in the starting state and throws if not. |
| 94 | + /// </summary> |
| 95 | + private void AssertStarting() |
| 96 | + { |
| 97 | + if (startTcs is null) |
| 98 | + { |
| 99 | + throw new InvalidOperationException("The process is not starting/started, use Start() first."); |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + public async Task<bool> Stop(CancellationToken token = default) |
| 104 | + { |
| 105 | + AssertStarting(); |
| 106 | + if (stopTcs is not null) |
| 107 | + { |
| 108 | + return await stopTcs.Task; |
| 109 | + } |
| 110 | + stopTcs = new(); |
| 111 | + token.ThrowIfCancellationRequested(); |
| 112 | + process.Kill(); |
| 113 | + await process.WaitForExitAsync(token); |
| 114 | + return true; |
| 115 | + } |
| 116 | +} |
0 commit comments