File: Interactive\InteractiveSession.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// 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 InteractiveHost;
extern alias Scripting;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Scripting.Hosting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Interactive;
 
using InteractiveHost::Microsoft.CodeAnalysis.Interactive;
using RelativePathResolver = Scripting::Microsoft.CodeAnalysis.RelativePathResolver;
 
internal sealed class InteractiveSession : IDisposable
{
    public InteractiveHost Host { get; }
 
    private readonly IThreadingContext _threadingContext;
    private readonly InteractiveEvaluatorLanguageInfoProvider _languageInfo;
    private readonly InteractiveWorkspace _workspace;
    private readonly ITextDocumentFactoryService _textDocumentFactoryService;
    private readonly EditorOptionsService _editorOptionsService;
 
    private readonly CancellationTokenSource _shutdownCancellationSource;
 
    /// <summary>
    /// The top level directory where all the interactive host extensions are installed (both Core and Desktop).
    /// </summary>
    private readonly string _hostDirectory;
 
    #region State only accessible by queued tasks
 
    // Use to serialize InteractiveHost process initialization and code execution.
    // The process may restart any time and we need to react to that by clearing 
    // the current solution and setting up the first submission project. 
    // At the same time a code submission might be in progress.
    // If we left these operations run in parallel we might get into a state
    // inconsistent with the state of the host.
    private readonly TaskQueue _taskQueue;
 
    private ProjectId? _lastSuccessfulSubmissionProjectId;
    private ProjectId? _currentSubmissionProjectId;
    public int SubmissionCount { get; private set; }
 
    private RemoteInitializationResult? _initializationResult;
    private InteractiveHostPlatformInfo _platformInfo;
    private ImmutableArray<string> _referenceSearchPaths;
    private ImmutableArray<string> _sourceSearchPaths;
    private string _workingDirectory;
    private InteractiveHostOptions? _hostOptions;
 
    /// <summary>
    /// Buffers that need to be associated with a submission project once the process initialization completes.
    /// </summary>
    private readonly List<(ITextBuffer buffer, string name)> _pendingBuffers = [];
 
    #endregion
 
    public InteractiveSession(
        InteractiveWorkspace workspace,
        IThreadingContext threadingContext,
        IAsynchronousOperationListener listener,
        ITextDocumentFactoryService documentFactory,
        EditorOptionsService editorOptionsService,
        InteractiveEvaluatorLanguageInfoProvider languageInfo,
        string initialWorkingDirectory)
    {
        _workspace = workspace;
        _threadingContext = threadingContext;
        _languageInfo = languageInfo;
        _textDocumentFactoryService = documentFactory;
        _editorOptionsService = editorOptionsService;
 
        _taskQueue = new TaskQueue(listener, TaskScheduler.Default);
        _shutdownCancellationSource = new CancellationTokenSource();
 
        // The following settings will apply when the REPL starts without .rsp file.
        // They are discarded once the REPL is reset.
        _referenceSearchPaths = [];
        _sourceSearchPaths = [];
        _workingDirectory = initialWorkingDirectory;
 
        _hostDirectory = Path.Combine(Path.GetDirectoryName(typeof(InteractiveSession).Assembly.Location)!, "InteractiveHost");
 
        Host = new InteractiveHost(languageInfo.ReplServiceProviderType, initialWorkingDirectory);
        Host.ProcessInitialized += ProcessInitialized;
    }
 
    public void Dispose()
    {
        _shutdownCancellationSource.Cancel();
        _shutdownCancellationSource.Dispose();
 
        Host.Dispose();
    }
 
    /// <summary>
    /// Invoked by <see cref="InteractiveHost"/> when a new process initialization completes.
    /// </summary>
    private void ProcessInitialized(InteractiveHostPlatformInfo platformInfo, InteractiveHostOptions options, RemoteExecutionResult result)
    {
        Contract.ThrowIfFalse(result.InitializationResult != null);
 
        _ = _taskQueue.ScheduleTask(nameof(ProcessInitialized), () =>
        {
            _workspace.ResetSolution();
 
            _currentSubmissionProjectId = null;
            _lastSuccessfulSubmissionProjectId = null;
 
            // update host state:
            _platformInfo = platformInfo;
            _initializationResult = result.InitializationResult;
            _hostOptions = options;
            UpdatePathsNoLock(result);
 
            // Create submission projects for buffers that were added by the Interactive Window 
            // before the process initialization completed.
            foreach (var (buffer, languageName) in _pendingBuffers)
            {
                AddSubmissionProjectNoLock(buffer, languageName);
            }
 
            _pendingBuffers.Clear();
        }, _shutdownCancellationSource.Token);
    }
 
    /// <summary>
    /// Invoked on UI thread when a new language buffer is created and before it is added to the projection.
    /// </summary>
    internal void AddSubmissionProject(ITextBuffer submissionBuffer)
    {
        _taskQueue.ScheduleTask(
            nameof(AddSubmissionProject),
            () => AddSubmissionProjectNoLock(submissionBuffer, _languageInfo.LanguageName),
            _shutdownCancellationSource.Token);
    }
 
    private void AddSubmissionProjectNoLock(ITextBuffer submissionBuffer, string languageName)
    {
        var initResult = _initializationResult;
 
        var imports = ImmutableArray<string>.Empty;
        var references = ImmutableArray<MetadataReference>.Empty;
 
        var initializationScriptImports = ImmutableArray<string>.Empty;
        var initializationScriptReferences = ImmutableArray<MetadataReference>.Empty;
 
        ProjectId? initializationScriptProjectId = null;
        string? initializationScriptPath = null;
 
        if (_currentSubmissionProjectId == null)
        {
            Debug.Assert(_lastSuccessfulSubmissionProjectId == null);
 
            // The Interactive Window may have added the first language buffer before 
            // the host initialization has completed. Do not create a submission project 
            // for the buffer in such case. It will be created when the initialization completes.
            if (initResult == null)
            {
                _pendingBuffers.Add((submissionBuffer, languageName));
                return;
            }
 
            imports = initResult.Imports.ToImmutableArrayOrEmpty();
 
            var metadataService = _workspace.Services.GetRequiredService<IMetadataService>();
            references = initResult.MetadataReferencePaths.ToImmutableArrayOrEmpty().SelectAsArray(
                (path, metadataService) => (MetadataReference)metadataService.GetReference(path, MetadataReferenceProperties.Assembly),
                metadataService);
 
            // if a script was specified in .rps file insert a project with a document that represents it:
            initializationScriptPath = initResult!.ScriptPath;
            if (initializationScriptPath != null)
            {
                initializationScriptProjectId = ProjectId.CreateNewId(CreateNewSubmissionName());
 
                _lastSuccessfulSubmissionProjectId = initializationScriptProjectId;
 
                // imports and references will be inherited:
                initializationScriptImports = imports;
                initializationScriptReferences = references;
                imports = [];
                references = [];
            }
        }
 
        var newSubmissionProjectName = CreateNewSubmissionName();
        var newSubmissionText = submissionBuffer.CurrentSnapshot.AsText();
        _currentSubmissionProjectId = ProjectId.CreateNewId(newSubmissionProjectName);
        var newSubmissionDocumentId = DocumentId.CreateNewId(_currentSubmissionProjectId, newSubmissionProjectName);
 
        // If the _initializationResult is not null we must also have the host options.
        RoslynDebug.AssertNotNull(_hostOptions);
        // Retrieve the directory that the host path exe is located in.
        var hostPathDirectory = Path.GetDirectoryName(_hostOptions.HostPath);
        RoslynDebug.AssertNotNull(hostPathDirectory);
 
        // Create a file path for the submission located in the interactive host directory.
        var newSubmissionFilePath = Path.Combine(hostPathDirectory, $"Submission{SubmissionCount}{_languageInfo.Extension}");
 
        // Associate the path with both the editor document and our roslyn document so LSP can make requests on it.
        _textDocumentFactoryService.TryGetTextDocument(submissionBuffer, out var textDocument);
        textDocument.Rename(newSubmissionFilePath);
 
        // Chain projects to the the last submission that successfully executed.
        _workspace.SetCurrentSolution(solution =>
        {
            if (initializationScriptProjectId != null)
            {
                RoslynDebug.AssertNotNull(initializationScriptPath);
 
                var initProject = CreateSubmissionProjectNoLock(solution, initializationScriptProjectId, previousSubmissionProjectId: null, languageName, initializationScriptImports, initializationScriptReferences);
                solution = initProject.Solution.AddDocument(
                    DocumentId.CreateNewId(initializationScriptProjectId, debugName: initializationScriptPath),
                    Path.GetFileName(initializationScriptPath),
                    new WorkspaceFileTextLoader(solution.Services, initializationScriptPath, defaultEncoding: null));
            }
 
            var newSubmissionProject = CreateSubmissionProjectNoLock(solution, _currentSubmissionProjectId, _lastSuccessfulSubmissionProjectId, languageName, imports, references);
            solution = newSubmissionProject.Solution.AddDocument(
                newSubmissionDocumentId,
                newSubmissionProjectName,
                newSubmissionText,
                filePath: newSubmissionFilePath);
 
            return solution;
        }, WorkspaceChangeKind.SolutionChanged);
 
        // opening document will start workspace listening to changes in this text container
        _workspace.OpenDocument(newSubmissionDocumentId, submissionBuffer.AsTextContainer());
    }
 
    private string CreateNewSubmissionName()
        => "Submission#" + SubmissionCount++;
 
    private Project CreateSubmissionProjectNoLock(Solution solution, ProjectId newSubmissionProjectId, ProjectId? previousSubmissionProjectId, string languageName, ImmutableArray<string> imports, ImmutableArray<MetadataReference> references)
    {
        var name = newSubmissionProjectId.DebugName;
        RoslynDebug.AssertNotNull(name);
 
        CompilationOptions compilationOptions;
        if (previousSubmissionProjectId != null)
        {
            compilationOptions = solution.GetRequiredProject(previousSubmissionProjectId).CompilationOptions!;
 
            var metadataResolver = (RuntimeMetadataReferenceResolver)compilationOptions.MetadataReferenceResolver!;
            if (metadataResolver.PathResolver.BaseDirectory != _workingDirectory ||
                !metadataResolver.PathResolver.SearchPaths.SequenceEqual(_referenceSearchPaths))
            {
                compilationOptions = compilationOptions.WithMetadataReferenceResolver(metadataResolver.WithRelativePathResolver(new RelativePathResolver(_referenceSearchPaths, _workingDirectory)));
            }
 
            var sourceResolver = (SourceFileResolver)compilationOptions.SourceReferenceResolver!;
            if (sourceResolver.BaseDirectory != _workingDirectory ||
                !sourceResolver.SearchPaths.SequenceEqual(_sourceSearchPaths))
            {
                compilationOptions = compilationOptions.WithSourceReferenceResolver(CreateSourceReferenceResolver(sourceResolver.SearchPaths, _workingDirectory));
            }
        }
        else
        {
            var metadataService = _workspace.Services.GetRequiredService<IMetadataService>();
            compilationOptions = _languageInfo.GetSubmissionCompilationOptions(
                name,
                CreateMetadataReferenceResolver(metadataService, _platformInfo, _referenceSearchPaths, _workingDirectory),
                CreateSourceReferenceResolver(_sourceSearchPaths, _workingDirectory),
                imports);
        }
 
        solution = solution.AddProject(
            ProjectInfo.Create(
                new ProjectInfo.ProjectAttributes(
                    id: newSubmissionProjectId,
                    version: VersionStamp.Create(),
                    name: name,
                    assemblyName: name,
                    language: languageName,
                    compilationOutputInfo: default,
                    checksumAlgorithm: SourceHashAlgorithms.Default,
                    isSubmission: true),
                compilationOptions: compilationOptions,
                parseOptions: _languageInfo.ParseOptions,
                documents: null,
                projectReferences: null,
                metadataReferences: references,
                hostObjectType: typeof(InteractiveScriptGlobals)));
 
        if (previousSubmissionProjectId != null)
        {
            solution = solution.AddProjectReference(newSubmissionProjectId, new ProjectReference(previousSubmissionProjectId));
        }
 
        return solution.GetRequiredProject(newSubmissionProjectId);
    }
 
    /// <summary>
    /// Called once a code snippet is submitted.
    /// Followed by creation of a new language buffer and call to <see cref="AddSubmissionProject(ITextBuffer)"/>.
    /// </summary>
    internal Task<bool> ExecuteCodeAsync(string text)
    {
        return _taskQueue.ScheduleTask(nameof(ExecuteCodeAsync), async () =>
        {
            var result = await Host.ExecuteAsync(text).ConfigureAwait(false);
            if (result.Success)
            {
                _lastSuccessfulSubmissionProjectId = _currentSubmissionProjectId;
 
                // update local search paths - remote paths has already been updated
                UpdatePathsNoLock(result);
            }
 
            return result.Success;
        }, _shutdownCancellationSource.Token);
    }
 
    internal async Task<bool> ResetAsync(InteractiveHostOptions options)
    {
        try
        {
            // Do not queue reset operation - invoke it directly.
            // Code execution might be in progress when the user requests reset (via a reset button, or process terminating on its own).
            // We need the execution to be interrupted and the process restarted, not wait for it to complete.
 
            var result = await Host.ResetAsync(options).ConfigureAwait(false);
 
            // Note: Not calling UpdatePathsNoLock here. The paths will be updated by ProcessInitialized 
            // which is executed once the new host process finishes its initialization.
 
            return result.Success;
        }
        catch (Exception e) when (FatalError.ReportAndPropagate(e))
        {
            throw ExceptionUtilities.Unreachable();
        }
    }
 
    public InteractiveHostOptions GetHostOptions(bool initialize, InteractiveHostPlatform? platform)
        => InteractiveHostOptions.CreateFromDirectory(
            _hostDirectory,
            initialize ? _languageInfo.InteractiveResponseFileName : null,
            CultureInfo.CurrentCulture,
            CultureInfo.CurrentUICulture,
             platform ?? Host.OptionsOpt?.Platform ?? InteractiveHost.DefaultPlatform);
 
    private static RuntimeMetadataReferenceResolver CreateMetadataReferenceResolver(IMetadataService metadataService, InteractiveHostPlatformInfo platformInfo, ImmutableArray<string> searchPaths, string baseDirectory)
    {
        return new RuntimeMetadataReferenceResolver(
            searchPaths,
            baseDirectory,
            gacFileResolver: platformInfo.HasGlobalAssemblyCache ? new GacFileResolver(preferredCulture: CultureInfo.CurrentCulture) : null,
            platformAssemblyPaths: platformInfo.PlatformAssemblyPaths,
            createFromFileFunc: metadataService.GetReference);
    }
 
    private static SourceReferenceResolver CreateSourceReferenceResolver(ImmutableArray<string> searchPaths, string baseDirectory)
        => new SourceFileResolver(searchPaths, baseDirectory);
 
    public Task SetPathsAsync(ImmutableArray<string> referenceSearchPaths, ImmutableArray<string> sourceSearchPaths, string workingDirectory)
    {
        return _taskQueue.ScheduleTask(nameof(ExecuteCodeAsync), async () =>
        {
            var result = await Host.SetPathsAsync(referenceSearchPaths, sourceSearchPaths, workingDirectory).ConfigureAwait(false);
            UpdatePathsNoLock(result);
        }, _shutdownCancellationSource.Token);
    }
 
    private void UpdatePathsNoLock(RemoteExecutionResult result)
    {
        _workingDirectory = result.WorkingDirectory;
        _referenceSearchPaths = result.ReferencePaths;
        _sourceSearchPaths = result.SourcePaths;
    }
}