|
// 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.
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Roslyn.Utilities;
using StreamJsonRpc;
namespace Microsoft.CodeAnalysis.Interactive
{
internal partial class InteractiveHost
{
internal sealed class RemoteService
{
private static readonly char[] s_unicodeAsciiTranscodingMarkerDesktop = EncodeMarker("\nProcess is terminated due to StackOverflowException.\n");
private static readonly char[] s_unicodeAsciiTranscodingMarkerCore = EncodeMarker("Stack overflow.\n");
public readonly Process Process;
public readonly JsonRpc JsonRpc;
public readonly InteractiveHostPlatformInfo PlatformInfo;
public readonly InteractiveHostOptions Options;
private readonly int _processId;
private readonly SemaphoreSlim _disposeSemaphore = new SemaphoreSlim(initialCount: 1);
private readonly bool _joinOutputWritingThreadsOnDisposal;
// output pumping threads (stream output from stdout/stderr of the host process to the output/errorOutput writers)
private InteractiveHost? _host; // nulled on dispose
private Thread? _readOutputThread; // nulled on dispose
private Thread? _readErrorOutputThread; // nulled on dispose
private volatile ProcessExitHandlerStatus _processExitHandlerStatus; // set to Handled on dispose
internal RemoteService(InteractiveHost host, Process process, int processId, JsonRpc jsonRpc, InteractiveHostPlatformInfo platformInfo, InteractiveHostOptions options)
{
Process = process;
JsonRpc = jsonRpc;
PlatformInfo = platformInfo;
Options = options;
_host = host;
_joinOutputWritingThreadsOnDisposal = _host._joinOutputWritingThreadsOnDisposal;
_processId = processId;
_processExitHandlerStatus = ProcessExitHandlerStatus.Uninitialized;
// TODO (tomat): consider using single-thread async readers
_readOutputThread = new Thread(() => ReadOutput(error: false));
_readOutputThread.Name = "InteractiveHost-OutputReader-" + processId;
_readOutputThread.IsBackground = true;
_readOutputThread.Start();
_readErrorOutputThread = new Thread(() => ReadOutput(error: true));
_readErrorOutputThread.Name = "InteractiveHost-ErrorOutputReader-" + processId;
_readErrorOutputThread.IsBackground = true;
_readErrorOutputThread.Start();
}
internal void HookAutoRestartEvent()
{
using (_disposeSemaphore.DisposableWait())
{
// hook the event only once per process:
if (_processExitHandlerStatus == ProcessExitHandlerStatus.Uninitialized)
{
Process.Exited += ProcessExitedHandler;
_processExitHandlerStatus = ProcessExitHandlerStatus.Hooked;
}
}
}
private void ProcessExitedHandler(object sender, EventArgs e)
{
_ = ProcessExitedHandlerAsync();
}
private async Task ProcessExitedHandlerAsync()
{
try
{
using (await _disposeSemaphore.DisposableWaitAsync().ConfigureAwait(false))
{
if (_processExitHandlerStatus == ProcessExitHandlerStatus.Hooked)
{
Process.Exited -= ProcessExitedHandler;
_processExitHandlerStatus = ProcessExitHandlerStatus.Handled;
// Should set _processExitHandlerStatus before calling OnProcessExited to avoid deadlocks.
// Calling the host should be within the lock to prevent its disposing during the execution.
}
}
var host = _host;
if (host != null)
{
await host.OnProcessExitedAsync(Process).ConfigureAwait(false);
}
}
catch (Exception e) when (FatalError.ReportAndPropagate(e))
{
throw ExceptionUtilities.Unreachable();
}
}
private static char[] EncodeMarker(string ascii)
{
Debug.Assert(ascii.Length % 2 == 0);
return Encoding.Unicode.GetChars(Encoding.ASCII.GetBytes(ascii));
}
private void ReadOutput(bool error)
{
const int BaseLength = 4096;
// Workaround for https://github.com/dotnet/runtime/issues/45503.
// When the process terminates due to stack overflow the CLR prints out a message that is not correctly encoded to Unicode.
// Hence it will come out as ASCII bytes interpreted as UTF-16 characters.
// We detect known message text in the output stream and transcode it and the output that follows to Unicode.
var transcodingMarker = Options.Platform == InteractiveHostPlatform.Core ?
s_unicodeAsciiTranscodingMarkerCore : s_unicodeAsciiTranscodingMarkerDesktop;
var buffer = new char[BaseLength + transcodingMarker.Length];
StreamReader reader = error ? Process.StandardError : Process.StandardOutput;
bool transcoding = false;
try
{
// loop until the output pipe is closed and has no more data (process is killed):
while (!reader.EndOfStream)
{
int charsRead = reader.Read(buffer, 0, BaseLength);
if (charsRead == 0)
{
break;
}
if (transcoding)
{
charsRead = Transcode(ref buffer, charsRead, 0);
}
else if (error)
{
int transcodingMarkerStart = Array.IndexOf(buffer, transcodingMarker[0], startIndex: 0, count: charsRead);
if (transcodingMarkerStart >= 0)
{
int additionalCharsToRead = transcodingMarkerStart + transcodingMarker.Length - charsRead;
if (additionalCharsToRead > 0)
{
charsRead += ReadAll(reader, buffer, index: charsRead, length: additionalCharsToRead);
}
if (ArraysEqual(buffer, transcodingMarkerStart, transcodingMarker, 0, transcodingMarker.Length))
{
// once we hit the marker we assume everything that follows is encoded:
charsRead = Transcode(ref buffer, charsRead, transcodingMarkerStart);
transcoding = true;
}
}
}
var localHost = _host;
if (localHost == null)
{
break;
}
localHost.OnOutputReceived(error, buffer, charsRead);
}
}
catch (Exception e)
{
Debug.WriteLine("InteractiveHostProcess: exception while reading output from process {0}: {1}", _processId, e.Message);
}
}
private static bool ArraysEqual(char[] left, int leftStart, char[] right, int rightStart, int length)
{
if (leftStart + length > left.Length || rightStart + length > right.Length)
{
return false;
}
for (int i = 0; i < length; i++)
{
if (left[leftStart + i] != right[rightStart + i])
{
return false;
}
}
return true;
}
private static int Transcode(ref char[] buffer, int bufferLength, int start)
{
var newBufferLength = start + (bufferLength - start) * 2;
if (buffer.Length < newBufferLength)
{
Array.Resize(ref buffer, newBufferLength);
}
for (int i = bufferLength - 1, j = newBufferLength - 2; i >= start; i--, j -= 2)
{
var c = buffer[i];
// Unicode is little endian:
buffer[j] = (char)(c & 0x00ff);
buffer[j + 1] = (char)(c >> 8);
}
return newBufferLength;
}
private static int ReadAll(StreamReader reader, char[] buffer, int index, int length)
{
var totalRead = 0;
while (!reader.EndOfStream && length > 0)
{
var charsRead = reader.Read(buffer, index, length);
if (charsRead == 0)
{
break;
}
totalRead += charsRead;
index += charsRead;
length -= charsRead;
}
return totalRead;
}
// Dispose may called anytime, on any thread.
internal void Dispose()
{
// There can be a call from host initiated from OnProcessExit.
// We should not proceed with disposing if _disposeSemaphore is locked.
using (_disposeSemaphore.DisposableWait())
{
if (_processExitHandlerStatus == ProcessExitHandlerStatus.Hooked)
{
Process.Exited -= ProcessExitedHandler;
_processExitHandlerStatus = ProcessExitHandlerStatus.Handled;
}
}
InitiateTermination(Process, _processId);
if (_joinOutputWritingThreadsOnDisposal)
{
try
{
_readOutputThread?.Join();
}
catch (ThreadStateException)
{
// thread hasn't started
}
try
{
_readErrorOutputThread?.Join();
}
catch (ThreadStateException)
{
// thread hasn't started
}
}
// null the host so that we don't attempt to write to the buffer anymore:
_host = null;
_readOutputThread = _readErrorOutputThread = null;
}
internal static void InitiateTermination(Process process, int processId)
{
try
{
if (!process.HasExited)
{
process.Kill();
}
}
catch (Exception e)
{
Debug.WriteLine("InteractiveHostProcess: can't terminate process {0}: {1}", processId, e.Message);
}
}
private enum ProcessExitHandlerStatus
{
Uninitialized = 0,
Hooked = 1,
Handled = 2
}
}
}
}
|