public static ServerRequest Create( string workingDirectory, string tempDirectory, IList <string> args, string keepAlive = null) { ServerLogger.Log("Creating ServerRequest"); ServerLogger.Log($"Working directory: {workingDirectory}"); ServerLogger.Log($"Temp directory: {tempDirectory}"); var requestLength = args.Count + 1; var requestArgs = new List <RequestArgument>(requestLength) { new RequestArgument(RequestArgument.ArgumentId.CurrentDirectory, 0, workingDirectory), new RequestArgument(RequestArgument.ArgumentId.TempDirectory, 0, tempDirectory) }; if (keepAlive != null) { requestArgs.Add(new RequestArgument(RequestArgument.ArgumentId.KeepAlive, 0, keepAlive)); } for (var i = 0; i < args.Count; ++i) { var arg = args[i]; ServerLogger.Log($"argument[{i}] = {arg}"); requestArgs.Add(new RequestArgument(RequestArgument.ArgumentId.CommandLineArgument, i, arg)); } return(new ServerRequest(ServerProtocol.ProtocolVersion, requestArgs)); }
public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken) { if (!TryParseArguments(request, out var parsed)) { return(new RejectedServerResponse()); } var exitCode = 0; var commandArgs = parsed.args.ToArray(); var outputWriter = new StringWriter(); var errorWriter = new StringWriter(); var checker = new DefaultExtensionDependencyChecker(Loader, outputWriter, errorWriter); var app = new Application(cancellationToken, Loader, checker, AssemblyReferenceProvider, outputWriter, errorWriter); exitCode = app.Execute(commandArgs); var output = outputWriter.ToString(); var error = errorWriter.ToString(); outputWriter.Dispose(); errorWriter.Dispose(); // This will no-op if server logging is not enabled. ServerLogger.Log(output); ServerLogger.Log(error); return(new CompletedServerResponse(exitCode, utf8output: false, output, error)); }
public async override Task WaitForDisconnectAsync(CancellationToken cancellationToken) { if (!(Stream is PipeStream pipeStream)) { return; } // We have to poll for disconnection by reading, PipeStream.IsConnected isn't reliable unless you // actually do a read - which will cause it to update its state. while (!cancellationToken.IsCancellationRequested && pipeStream.IsConnected) { await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken); try { ServerLogger.Log($"Before poking pipe {Identifier}."); await Stream.ReadAsync(Array.Empty <byte>(), 0, 0, cancellationToken); ServerLogger.Log($"After poking pipe {Identifier}."); } catch (OperationCanceledException) { } catch (Exception e) { // It is okay for this call to fail. Errors will be reflected in the // IsConnected property which will be read on the next iteration. ServerLogger.LogException(e, $"Error poking pipe {Identifier}."); } } }
public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken) { if (!TryParseArguments(request, out var parsed)) { return(new RejectedServerResponse()); } var exitCode = 0; var output = string.Empty; var commandArgs = parsed.args.ToArray(); var writer = ServerLogger.IsLoggingEnabled ? new StringWriter() : TextWriter.Null; var checker = new DefaultExtensionDependencyChecker(Loader, writer); var app = new Application(cancellationToken, Loader, checker) { Out = writer, Error = writer, }; exitCode = app.Execute(commandArgs); if (ServerLogger.IsLoggingEnabled) { output = writer.ToString(); ServerLogger.Log(output); } return(new CompletedServerResponse(exitCode, utf8output: false, output: string.Empty)); }
/// <summary> /// This task does not complete until we are completely done reading. /// </summary> internal static async Task ReadAllAsync( Stream stream, byte[] buffer, int count, CancellationToken cancellationToken) { var totalBytesRead = 0; do { ServerLogger.Log("Attempting to read {0} bytes from the stream", count - totalBytesRead); var bytesRead = await stream.ReadAsync( buffer, totalBytesRead, count - totalBytesRead, cancellationToken) .ConfigureAwait(false); if (bytesRead == 0) { ServerLogger.Log("Unexpected -- read 0 bytes from the stream."); throw new EndOfStreamException("Reached end of stream before end of read."); } ServerLogger.Log("Read {0} bytes", bytesRead); totalBytesRead += bytesRead; } while (totalBytesRead < count); ServerLogger.Log("Finished read"); }
public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken) { if (!TryParseArguments(request, out var parsed)) { return(new RejectedServerResponse()); } var exitCode = 0; var output = string.Empty; var app = new Application(cancellationToken); var commandArgs = parsed.args.ToArray(); if (ServerLogger.IsLoggingEnabled) { using (var writer = new StringWriter()) { app.Out = writer; app.Error = writer; exitCode = app.Execute(commandArgs); output = writer.ToString(); ServerLogger.Log(output); } } else { using (var writer = new StreamWriter(Stream.Null)) { app.Out = writer; app.Error = writer; exitCode = app.Execute(commandArgs); } } return(new CompletedServerResponse(exitCode, utf8output: false, output: string.Empty)); }
public async override Task <Connection> WaitForConnectionAsync(CancellationToken cancellationToken) { // Create the pipe and begin waiting for a connection. This doesn't block, but could fail // in certain circumstances, such as the OS refusing to create the pipe for some reason // or the pipe was disconnected before we starting listening. var pipeStream = new NamedPipeServerStream( PipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, // Maximum connections. PipeTransmissionMode.Byte, _pipeOptions, PipeBufferSize, // Default input buffer PipeBufferSize); // Default output buffer ServerLogger.Log("Waiting for new connection"); await pipeStream.WaitForConnectionAsync(cancellationToken); ServerLogger.Log("Pipe connection detected."); if (Environment.Is64BitProcess || Memory.IsMemoryAvailable()) { ServerLogger.Log("Memory available - accepting connection"); return(new NamedPipeConnection(pipeStream, GetNextIdentifier())); } pipeStream.Close(); throw new Exception("Insufficient resources to process new connection."); }
public async Task WriteAsync(Stream outStream, CancellationToken cancellationToken) { using (var memoryStream = new MemoryStream()) using (var writer = new BinaryWriter(memoryStream, Encoding.Unicode)) { // Format the response ServerLogger.Log("Formatting Response"); writer.Write((int)Type); AddResponseBody(writer); writer.Flush(); cancellationToken.ThrowIfCancellationRequested(); // Send the response to the client // Write the length of the response var length = checked ((int)memoryStream.Length); ServerLogger.Log("Writing response length"); // There is no way to know the number of bytes written to // the pipe stream. We just have to assume all of them are written. await outStream .WriteAsync(BitConverter.GetBytes(length), 0, 4, cancellationToken) .ConfigureAwait(false); // Write the response ServerLogger.Log("Writing response of size {0}", length); memoryStream.Position = 0; await memoryStream .CopyToAsync(outStream, bufferSize : length, cancellationToken : cancellationToken) .ConfigureAwait(false); } }
/// <summary> /// Try to process the request using the server. Returns a null-containing Task if a response /// from the server cannot be retrieved. /// </summary> private static async Task <ServerResponse> TryProcessRequest( Client client, ServerRequest request, CancellationToken cancellationToken) { ServerResponse response; using (client) { // Write the request try { ServerLogger.Log("Begin writing request"); await request.WriteAsync(client.Stream, cancellationToken).ConfigureAwait(false); ServerLogger.Log("End writing request"); } catch (Exception e) { ServerLogger.LogException(e, "Error writing build request."); return(new RejectedServerResponse()); } // Wait for the compilation and a monitor to detect if the server disconnects var serverCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); ServerLogger.Log("Begin reading response"); var responseTask = ServerResponse.ReadAsync(client.Stream, serverCts.Token); var monitorTask = client.WaitForDisconnectAsync(serverCts.Token); await Task.WhenAny(new[] { responseTask, monitorTask }).ConfigureAwait(false); ServerLogger.Log("End reading response"); if (responseTask.IsCompleted) { // await the task to log any exceptions try { response = await responseTask.ConfigureAwait(false); } catch (Exception e) { ServerLogger.LogException(e, "Error reading response"); response = new RejectedServerResponse(); } } else { ServerLogger.Log("Server disconnect"); response = new RejectedServerResponse(); } // Cancel whatever task is still around serverCts.Cancel(); Debug.Assert(response != null); return(response); } }
// Based on: https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Shared/BuildServerConnection.cs#L290 public static async Task <Client> ConnectAsync(string pipeName, TimeSpan?timeout, CancellationToken cancellationToken) { var timeoutMilliseconds = timeout == null ? Timeout.Infinite : (int)timeout.Value.TotalMilliseconds; try { // Machine-local named pipes are named "\\.\pipe\<pipename>". // We use the SHA1 of the directory the compiler exes live in as the pipe name. // The NamedPipeClientStream class handles the "\\.\pipe\" part for us. ServerLogger.Log("Attempt to open named pipe '{0}'", pipeName); var stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, _pipeOptions); cancellationToken.ThrowIfCancellationRequested(); ServerLogger.Log("Attempt to connect named pipe '{0}'", pipeName); try { await stream.ConnectAsync(timeoutMilliseconds, cancellationToken); } catch (Exception e) when(e is IOException || e is TimeoutException) { // Note: IOException can also indicate timeout. // From docs: // - TimeoutException: Could not connect to the server within the specified timeout period. // - IOException: The server is connected to another client and the time-out period has expired. ServerLogger.Log($"Connecting to server timed out after {timeoutMilliseconds} ms"); return(null); } ServerLogger.Log("Named pipe '{0}' connected", pipeName); cancellationToken.ThrowIfCancellationRequested(); #if NETFRAMEWORK // Verify that we own the pipe. if (!CheckPipeConnectionOwnership(stream)) { ServerLogger.Log("Owner of named pipe is incorrect"); return(null); } #endif return(new NamedPipeClient(stream, GetNextIdentifier())); } catch (Exception e) when(!(e is TaskCanceledException || e is OperationCanceledException)) { ServerLogger.LogException(e, "Exception while connecting to process"); return(null); } }
protected override void Dispose(bool disposing) { ServerLogger.Log($"Pipe {Identifier}: Closing."); try { Stream.Dispose(); } catch (Exception ex) { // The client connection failing to close isn't fatal to the server process. It is simply a client // for which we can no longer communicate and that's okay because the Close method indicates we are // done with the client already. var message = $"Pipe {Identifier}: Error closing pipe."; ServerLogger.LogException(ex, message); } }
private Task <ServerResponse> ExecuteRequestAsync(ServerRequest buildRequest, CancellationToken cancellationToken) { Func <ServerResponse> func = () => { ServerLogger.Log("Begin processing request"); var response = _compilerHost.Execute(buildRequest, cancellationToken); ServerLogger.Log("End processing request"); return(response); }; var task = new Task <ServerResponse>(func, cancellationToken, TaskCreationOptions.LongRunning); task.Start(); return(task); }
// Based on: https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Shared/BuildServerConnection.cs#L290 public static async Task <Client> ConnectAsync(string pipeName, TimeSpan?timeout, CancellationToken cancellationToken) { var timeoutMilliseconds = timeout == null ? Timeout.Infinite : (int)timeout.Value.TotalMilliseconds; try { // Machine-local named pipes are named "\\.\pipe\<pipename>". // We use the SHA1 of the directory the compiler exes live in as the pipe name. // The NamedPipeClientStream class handles the "\\.\pipe\" part for us. ServerLogger.Log("Attempt to open named pipe '{0}'", pipeName); var stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, _pipeOptions); cancellationToken.ThrowIfCancellationRequested(); ServerLogger.Log("Attempt to connect named pipe '{0}'", pipeName); try { await stream.ConnectAsync(timeoutMilliseconds, cancellationToken); } catch (Exception e) when(e is IOException || e is TimeoutException) { // Note: IOException can also indicate timeout. // From docs: // - TimeoutException: Could not connect to the server within the specified timeout period. // - IOException: The server is connected to another client and the time-out period has expired. ServerLogger.Log($"Connecting to server timed out after {timeoutMilliseconds} ms"); return(null); } ServerLogger.Log("Named pipe '{0}' connected", pipeName); cancellationToken.ThrowIfCancellationRequested(); // The original code in Roslyn checks that the server pipe is owned by the same user for security. // We plan to rely on the BCL for this but it's not yet implemented: // See https://github.com/dotnet/corefx/issues/25427 return(new NamedPipeClient(stream, GetNextIdentifier())); } catch (Exception e) when(!(e is TaskCanceledException || e is OperationCanceledException)) { ServerLogger.LogException(e, "Exception while connecting to process"); return(null); } }
/// <summary> /// Read a Request from the given stream. /// /// The total request size must be less than 1MB. /// </summary> /// <returns>null if the Request was too large, the Request otherwise.</returns> public static async Task <ServerRequest> ReadAsync(Stream inStream, CancellationToken cancellationToken) { // Read the length of the request var lengthBuffer = new byte[4]; ServerLogger.Log("Reading length of request"); await ServerProtocol.ReadAllAsync(inStream, lengthBuffer, 4, cancellationToken).ConfigureAwait(false); var length = BitConverter.ToInt32(lengthBuffer, 0); // Back out if the request is > 1MB if (length > 0x100000) { ServerLogger.Log("Request is over 1MB in length, cancelling read."); return(null); } cancellationToken.ThrowIfCancellationRequested(); // Read the full request var requestBuffer = new byte[length]; await ServerProtocol.ReadAllAsync(inStream, requestBuffer, length, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); ServerLogger.Log("Parsing request"); // Parse the request into the Request data structure. using (var reader = new BinaryReader(new MemoryStream(requestBuffer), Encoding.Unicode)) { var protocolVersion = reader.ReadUInt32(); var argumentCount = reader.ReadUInt32(); var argumentsBuilder = new List <RequestArgument>((int)argumentCount); for (var i = 0; i < argumentCount; i++) { cancellationToken.ThrowIfCancellationRequested(); argumentsBuilder.Add(RequestArgument.ReadFromBinaryReader(reader)); } return(new ServerRequest(protocolVersion, argumentsBuilder)); } }
/// <summary> /// May throw exceptions if there are pipe problems. /// </summary> /// <param name="stream"></param> /// <param name="cancellationToken"></param> /// <returns></returns> public static async Task <ServerResponse> ReadAsync(Stream stream, CancellationToken cancellationToken = default(CancellationToken)) { ServerLogger.Log("Reading response length"); // Read the response length var lengthBuffer = new byte[4]; await ServerProtocol.ReadAllAsync(stream, lengthBuffer, 4, cancellationToken).ConfigureAwait(false); var length = BitConverter.ToUInt32(lengthBuffer, 0); // Read the response ServerLogger.Log("Reading response of length {0}", length); var responseBuffer = new byte[length]; await ServerProtocol.ReadAllAsync( stream, responseBuffer, responseBuffer.Length, cancellationToken) .ConfigureAwait(false); using (var reader = new BinaryReader(new MemoryStream(responseBuffer), Encoding.Unicode)) { var responseType = (ResponseType)reader.ReadInt32(); switch (responseType) { case ResponseType.Completed: return(CompletedServerResponse.Create(reader)); case ResponseType.MismatchedVersion: return(new MismatchedVersionServerResponse()); case ResponseType.Shutdown: return(ShutdownServerResponse.Create(reader)); case ResponseType.Rejected: return(new RejectedServerResponse()); default: throw new InvalidOperationException("Received invalid response type from server."); } } }
/// <summary> /// Write a Request to the stream. /// </summary> public async Task WriteAsync(Stream outStream, CancellationToken cancellationToken = default(CancellationToken)) { using (var memoryStream = new MemoryStream()) using (var writer = new BinaryWriter(memoryStream, Encoding.Unicode)) { // Format the request. ServerLogger.Log("Formatting request"); writer.Write(ProtocolVersion); writer.Write(Arguments.Count); foreach (var arg in Arguments) { cancellationToken.ThrowIfCancellationRequested(); arg.WriteToBinaryWriter(writer); } writer.Flush(); cancellationToken.ThrowIfCancellationRequested(); // Write the length of the request var length = checked ((int)memoryStream.Length); // Back out if the request is > 1 MB if (memoryStream.Length > 0x100000) { ServerLogger.Log("Request is over 1MB in length, cancelling write"); throw new ArgumentOutOfRangeException(); } // Send the request to the server ServerLogger.Log("Writing length of request."); await outStream .WriteAsync(BitConverter.GetBytes(length), 0, 4, cancellationToken) .ConfigureAwait(false); ServerLogger.Log("Writing request of size {0}", length); // Write the request memoryStream.Position = 0; await memoryStream .CopyToAsync(outStream, bufferSize : length, cancellationToken : cancellationToken) .ConfigureAwait(false); } }
public static int Main(string[] args) { DebugMode.HandleDebugSwitch(ref args); var cancel = new CancellationTokenSource(); Console.CancelKeyPress += (sender, e) => { cancel.Cancel(); }; var outputWriter = new StringWriter(); var errorWriter = new StringWriter(); // Prevent shadow copying. var loader = new DefaultExtensionAssemblyLoader(baseDirectory: null); var checker = new DefaultExtensionDependencyChecker(loader, outputWriter, errorWriter); var application = new Application( cancel.Token, loader, checker, (path, properties) => MetadataReference.CreateFromFile(path, properties), outputWriter, errorWriter); var result = application.Execute(args); var output = outputWriter.ToString(); var error = errorWriter.ToString(); outputWriter.Dispose(); errorWriter.Dispose(); Console.Write(output); Console.Error.Write(error); // This will no-op if server logging is not enabled. ServerLogger.Log(output); ServerLogger.Log(error); return(result); }
// Internal for testing. internal static bool TryCreateServerCore(string clientDir, string pipeName, out int?processId, bool debug = false) { processId = null; // The server should be in the same directory as the client var expectedCompilerPath = Path.Combine(clientDir, ServerName); var expectedPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") ?? "dotnet"; var argumentList = new string[] { expectedCompilerPath, debug ? "--debug" : "", "server", "-p", pipeName }; var processArguments = ArgumentEscaper.EscapeAndConcatenate(argumentList); if (!File.Exists(expectedCompilerPath)) { return(false); } if (PlatformInformation.IsWindows) { // Currently, there isn't a way to use the Process class to create a process without // inheriting handles(stdin/stdout/stderr) from its parent. This might cause the parent process // to block on those handles. So we use P/Invoke. This code was taken from MSBuild task starting code. // The work to customize this behavior is being tracked by https://github.com/dotnet/corefx/issues/306. var startInfo = new STARTUPINFO(); startInfo.cb = Marshal.SizeOf(startInfo); startInfo.hStdError = NativeMethods.InvalidIntPtr; startInfo.hStdInput = NativeMethods.InvalidIntPtr; startInfo.hStdOutput = NativeMethods.InvalidIntPtr; startInfo.dwFlags = NativeMethods.STARTF_USESTDHANDLES; var dwCreationFlags = NativeMethods.NORMAL_PRIORITY_CLASS | NativeMethods.CREATE_NO_WINDOW; ServerLogger.Log("Attempting to create process '{0}'", expectedPath); var builder = new StringBuilder($@"""{expectedPath}"" {processArguments}"); var success = NativeMethods.CreateProcess( lpApplicationName: null, lpCommandLine: builder, lpProcessAttributes: NativeMethods.NullPtr, lpThreadAttributes: NativeMethods.NullPtr, bInheritHandles: false, dwCreationFlags: dwCreationFlags, lpEnvironment: NativeMethods.NullPtr, // Inherit environment lpCurrentDirectory: clientDir, lpStartupInfo: ref startInfo, lpProcessInformation: out var processInfo); if (success) { ServerLogger.Log("Successfully created process with process id {0}", processInfo.dwProcessId); NativeMethods.CloseHandle(processInfo.hProcess); NativeMethods.CloseHandle(processInfo.hThread); processId = processInfo.dwProcessId; } else { ServerLogger.Log("Failed to create process. GetLastError={0}", Marshal.GetLastWin32Error()); } return(success); } else { try { var startInfo = new ProcessStartInfo() { FileName = expectedPath, Arguments = processArguments, UseShellExecute = false, WorkingDirectory = clientDir, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; var process = Process.Start(startInfo); processId = process.Id; return(true); } catch { return(false); } } }
internal async Task <ConnectionResult> AcceptConnection(Task <Connection> task, bool accept, CancellationToken cancellationToken) { Connection connection; try { connection = await task; } catch (Exception ex) { // Unable to establish a connection with the client. The client is responsible for // handling this case. Nothing else for us to do here. ServerLogger.LogException(ex, "Error creating client named pipe"); return(new ConnectionResult(ConnectionResult.Reason.CompilationNotStarted)); } try { using (connection) { ServerRequest request; try { ServerLogger.Log("Begin reading request."); request = await ServerRequest.ReadAsync(connection.Stream, cancellationToken).ConfigureAwait(false); ServerLogger.Log("End reading request."); } catch (Exception e) { ServerLogger.LogException(e, "Error reading build request."); return(new ConnectionResult(ConnectionResult.Reason.CompilationNotStarted)); } if (request.IsShutdownRequest()) { // Reply with the PID of this process so that the client can wait for it to exit. var response = new ShutdownServerResponse(Process.GetCurrentProcess().Id); await response.WriteAsync(connection.Stream, cancellationToken); // We can safely disconnect the client, then when this connection gets cleaned up by the event loop // the server will go to a shutdown state. return(new ConnectionResult(ConnectionResult.Reason.ClientShutdownRequest)); } else if (!accept) { // We're already in shutdown mode, respond gracefully so the client can run in-process. var response = new RejectedServerResponse(); await response.WriteAsync(connection.Stream, cancellationToken).ConfigureAwait(false); return(new ConnectionResult(ConnectionResult.Reason.CompilationNotStarted)); } else { // If we get here then this is a real request that we will accept and process. // // Kick off both the compilation and a task to monitor the pipe for closing. var buildCancelled = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var watcher = connection.WaitForDisconnectAsync(buildCancelled.Token); var worker = ExecuteRequestAsync(request, buildCancelled.Token); // await will end when either the work is complete or the connection is closed. await Task.WhenAny(worker, watcher); // Do an 'await' on the completed task, preference being compilation, to force // any exceptions to be realized in this method for logging. ConnectionResult.Reason reason; if (worker.IsCompleted) { var response = await worker; try { ServerLogger.Log("Begin writing response."); await response.WriteAsync(connection.Stream, cancellationToken); ServerLogger.Log("End writing response."); reason = ConnectionResult.Reason.CompilationCompleted; _eventBus.CompilationCompleted(); } catch { reason = ConnectionResult.Reason.ClientDisconnect; } } else { await watcher; reason = ConnectionResult.Reason.ClientDisconnect; } // Begin the tear down of the Task which didn't complete. buildCancelled.Cancel(); return(new ConnectionResult(reason, request.KeepAlive)); } } } catch (Exception ex) { ServerLogger.LogException(ex, "Error handling connection"); return(new ConnectionResult(ConnectionResult.Reason.ClientException)); } }