File: Interactive\Core\InteractiveHost.LazyRemoteService.cs
Web Access
Project: src\src\Interactive\Host\Microsoft.CodeAnalysis.InteractiveHost.csproj (Microsoft.CodeAnalysis.InteractiveHost)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
extern alias Scripting;
 
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Roslyn.Utilities;
using StreamJsonRpc;
using Scripting::Microsoft.CodeAnalysis.Scripting.Hosting;
 
namespace Microsoft.CodeAnalysis.Interactive
{
    internal partial class InteractiveHost
    {
        private sealed class LazyRemoteService
        {
            private readonly AsyncLazy<InitializedRemoteService> _lazyInitializedService;
            private readonly CancellationTokenSource _cancellationSource;
 
            public readonly InteractiveHostOptions Options;
            public readonly InteractiveHost Host;
            public readonly bool SkipInitialization;
            public readonly int InstanceId;
 
            public LazyRemoteService(InteractiveHost host, InteractiveHostOptions options, int instanceId, bool skipInitialization)
            {
                _lazyInitializedService = AsyncLazy.Create(static (self, cancellationToken) => self.TryStartAndInitializeProcessAsync(cancellationToken), this);
                _cancellationSource = new CancellationTokenSource();
                InstanceId = instanceId;
                Options = options;
                Host = host;
                SkipInitialization = skipInitialization;
            }
 
            public void Dispose()
            {
                // Cancel the creation of the process if it is in progress.
                // If it is the cancellation will clean up all resources allocated during the creation.
                _cancellationSource.Cancel();
 
                // If the value has been calculated already, dispose the service.
                if (_lazyInitializedService.TryGetValue(out var initializedService))
                {
                    initializedService.Service?.Dispose();
                }
            }
 
            internal Task<InitializedRemoteService> GetInitializedServiceAsync()
                => _lazyInitializedService.GetValueAsync(_cancellationSource.Token);
 
            internal InitializedRemoteService? TryGetInitializedService()
                => _lazyInitializedService.TryGetValue(out var service) ? service : default;
 
            private async Task<InitializedRemoteService> TryStartAndInitializeProcessAsync(CancellationToken cancellationToken)
            {
                try
                {
                    var remoteService = await TryStartProcessAsync(Options.HostPath, Options.Culture, Options.UICulture, cancellationToken).ConfigureAwait(false);
                    if (remoteService == null)
                    {
                        return default;
                    }
 
                    RemoteExecutionResult result;
 
                    if (SkipInitialization)
                    {
                        result = new RemoteExecutionResult(
                            success: true,
                            sourcePaths: ImmutableArray<string>.Empty,
                            referencePaths: ImmutableArray<string>.Empty,
                            workingDirectory: Host._initialWorkingDirectory,
                            initializationResult: new RemoteInitializationResult(
                                initializationScript: null,
                                metadataReferencePaths: ImmutableArray.Create(typeof(object).Assembly.Location, typeof(InteractiveScriptGlobals).Assembly.Location),
                                imports: ImmutableArray<string>.Empty));
 
                        Host.ProcessInitialized?.Invoke(remoteService.PlatformInfo, Options, result);
                        return new InitializedRemoteService(remoteService, result);
                    }
 
                    bool initializing = true;
                    cancellationToken.Register(() =>
                    {
                        if (initializing)
                        {
                            // kill the process without triggering auto-reset:
                            remoteService.Dispose();
                        }
                    });
 
                    // try to execute initialization script:
                    var isRestarting = InstanceId > 1;
                    result = await ExecuteRemoteAsync(remoteService, nameof(Service.InitializeContextAsync), Options.InitializationFilePath, isRestarting).ConfigureAwait(false);
 
                    initializing = false;
                    if (!result.Success)
                    {
                        Host.ReportProcessExited(remoteService.Process);
                        remoteService.Dispose();
 
                        return default;
                    }
 
                    Contract.ThrowIfNull(result.InitializationResult);
 
                    // Hook up a handler that initiates restart when the process exits.
                    // Note that this is just so that we restart the process as soon as we see it dying and it doesn't need to be 100% bullet-proof.
                    // If we don't receive the "process exited" event we will restart the process upon the next remote operation.
                    remoteService.HookAutoRestartEvent();
 
                    Host.ProcessInitialized?.Invoke(remoteService.PlatformInfo, Options, result);
 
                    return new InitializedRemoteService(remoteService, result);
                }
#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods
                // await ExecuteRemoteAsync above does not take cancellationToken
                // - we don't currently support cancellation of the RPC call,
                // but JsonRpc.InvokeAsync that we use still claims it may throw OperationCanceledException..
                catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e))
#pragma warning restore CA2016
                {
                    throw ExceptionUtilities.Unreachable();
                }
            }
 
            private async Task<RemoteService?> TryStartProcessAsync(string hostPath, CultureInfo culture, CultureInfo uiCulture, CancellationToken cancellationToken)
            {
                int currentProcessId = Process.GetCurrentProcess().Id;
                var pipeName = typeof(InteractiveHost).FullName + Guid.NewGuid();
 
                var newProcess = new Process
                {
                    StartInfo = new ProcessStartInfo(hostPath)
                    {
                        Arguments = $"{pipeName} {currentProcessId} \"{culture.Name}\" \"{uiCulture.Name}\"",
                        WorkingDirectory = Host._initialWorkingDirectory,
                        CreateNoWindow = true,
                        UseShellExecute = false,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true,
                        StandardErrorEncoding = OutputEncoding,
                        StandardOutputEncoding = OutputEncoding
                    },
 
                    // enables Process.Exited event to be raised:
                    EnableRaisingEvents = true
                };
 
                try
                {
                    newProcess.Start();
                }
                catch (Exception e)
                {
                    Host.WriteOutputInBackground(
                        isError: true,
                        string.Format(InteractiveHostResources.Failed_to_create_a_remote_process_for_interactive_code_execution, hostPath),
                        e.Message);
 
                    Host.InteractiveHostProcessCreationFailed?.Invoke(e, TryGetExitCode(newProcess));
                    return null;
                }
 
                Host.InteractiveHostProcessCreated?.Invoke(newProcess);
 
                int newProcessId = -1;
                try
                {
                    newProcessId = newProcess.Id;
                }
                catch
                {
                    newProcessId = 0;
                }
 
                var clientStream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
                JsonRpc? jsonRpc = null;
 
                void ProcessExitedBeforeEstablishingConnection(object sender, EventArgs e)
                {
                    Host.InteractiveHostProcessCreationFailed?.Invoke(null, TryGetExitCode(newProcess));
                    _cancellationSource.Cancel();
                }
 
                // Connecting the named pipe client would block if the process exits before the connection is established,
                // as the client waits for the server to become available. We signal the cancellation token to abort.
                newProcess.Exited += ProcessExitedBeforeEstablishingConnection;
 
                InteractiveHostPlatformInfo platformInfo;
                try
                {
                    if (!CheckAlive(newProcess, hostPath))
                    {
                        Host.InteractiveHostProcessCreationFailed?.Invoke(null, TryGetExitCode(newProcess));
                        return null;
                    }
 
                    await clientStream.ConnectAsync(cancellationToken).ConfigureAwait(false);
                    jsonRpc = CreateRpc(clientStream, incomingCallTarget: null);
 
                    platformInfo = (await jsonRpc.InvokeWithCancellationAsync<InteractiveHostPlatformInfo.Data>(
                        nameof(Service.InitializeAsync),
                        new object[] { Host._replServiceProviderType.AssemblyQualifiedName },
                        cancellationToken).ConfigureAwait(false)).Deserialize();
                }
                catch (Exception e)
                {
                    if (CheckAlive(newProcess, hostPath))
                    {
                        RemoteService.InitiateTermination(newProcess, newProcessId);
                    }
 
                    jsonRpc?.Dispose();
 
                    Host.InteractiveHostProcessCreationFailed?.Invoke(e, TryGetExitCode(newProcess));
                    return null;
                }
                finally
                {
                    newProcess.Exited -= ProcessExitedBeforeEstablishingConnection;
                }
 
                return new RemoteService(Host, newProcess, newProcessId, jsonRpc, platformInfo, Options);
            }
 
            private bool CheckAlive(Process process, string hostPath)
            {
                bool alive = process.IsAlive();
                if (!alive)
                {
                    string errorString = process.StandardError.ReadToEnd();
 
                    Host.WriteOutputInBackground(
                        isError: true,
                        string.Format(InteractiveHostResources.Failed_to_launch_0_process_exit_code_colon_1_with_output_colon, hostPath, process.ExitCode),
                        errorString);
                }
 
                return alive;
            }
 
            private static int? TryGetExitCode(Process process)
            {
                try
                {
                    return process.ExitCode;
                }
                catch
                {
                    return null;
                }
            }
        }
    }
}