// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.CommandLineUtils; namespace Microsoft.AspNetCore.Razor.Tools { internal static class ServerConnection { private const string ServerName = "rzc.dll"; // Spend up to 1s connecting to existing process (existing processes should be always responsive). private const int TimeOutMsExistingProcess = 1000; // Spend up to 20s connecting to a new process, to allow time for it to start. private const int TimeOutMsNewProcess = 20000; // Custom delegate that contains an out param to use with TryCreateServerCore method. private delegate TResult TryCreateServerCoreDelegate(T1 arg1, T2 arg2, out T3 arg3, T4 arg4); public static bool WasServerMutexOpen(string mutexName) { Mutex mutex = null; var open = false; try { open = Mutex.TryOpenExisting(mutexName, out mutex); } catch { // In the case an exception occured trying to open the Mutex then // the assumption is that it's not open. } mutex?.Dispose(); return open; } /// /// Gets the value of the temporary path for the current environment assuming the working directory /// is . This function must emulate as /// closely as possible. /// public static string GetTempPath(string workingDir) { if (PlatformInformation.IsUnix) { // Unix temp path is fine: it does not use the working directory // (it uses ${TMPDIR} if set, otherwise, it returns /tmp) return Path.GetTempPath(); } var tmp = Environment.GetEnvironmentVariable("TMP"); if (Path.IsPathRooted(tmp)) { return tmp; } var temp = Environment.GetEnvironmentVariable("TEMP"); if (Path.IsPathRooted(temp)) { return temp; } if (!string.IsNullOrEmpty(workingDir)) { if (!string.IsNullOrEmpty(tmp)) { return Path.Combine(workingDir, tmp); } if (!string.IsNullOrEmpty(temp)) { return Path.Combine(workingDir, temp); } } var userProfile = Environment.GetEnvironmentVariable("USERPROFILE"); if (Path.IsPathRooted(userProfile)) { return userProfile; } return Environment.GetEnvironmentVariable("SYSTEMROOT"); } public static Task RunOnServer( string pipeName, IList arguments, ServerPaths serverPaths, CancellationToken cancellationToken, string keepAlive = null, bool debug = false) { if (string.IsNullOrEmpty(pipeName)) { pipeName = PipeName.ComputeDefault(serverPaths.ClientDirectory); } return RunOnServerCore( arguments, serverPaths, pipeName: pipeName, keepAlive: keepAlive, timeoutOverride: null, tryCreateServerFunc: TryCreateServerCore, cancellationToken: cancellationToken, debug: debug); } private static async Task RunOnServerCore( IList arguments, ServerPaths serverPaths, string pipeName, string keepAlive, int? timeoutOverride, TryCreateServerCoreDelegate tryCreateServerFunc, CancellationToken cancellationToken, bool debug) { if (pipeName == null) { return new RejectedServerResponse(); } if (serverPaths.TempDirectory == null) { return new RejectedServerResponse(); } var clientDir = serverPaths.ClientDirectory; var timeoutNewProcess = timeoutOverride ?? TimeOutMsNewProcess; var timeoutExistingProcess = timeoutOverride ?? TimeOutMsExistingProcess; var clientMutexName = MutexName.GetClientMutexName(pipeName); Task pipeTask = null; Mutex clientMutex = null; var holdsMutex = false; try { try { clientMutex = new Mutex(initiallyOwned: true, name: clientMutexName, createdNew: out holdsMutex); } catch (Exception ex) { // The Mutex constructor can throw in certain cases. One specific example is docker containers // where the /tmp directory is restricted. In those cases there is no reliable way to execute // the server and we need to fall back to the command line. // Example: https://github.com/dotnet/roslyn/issues/24124 ServerLogger.LogException(ex, "Client mutex creation failed."); return new RejectedServerResponse(); } if (!holdsMutex) { try { holdsMutex = clientMutex.WaitOne(timeoutNewProcess); if (!holdsMutex) { return new RejectedServerResponse(); } } catch (AbandonedMutexException) { holdsMutex = true; } } // Check for an already running server var serverMutexName = MutexName.GetServerMutexName(pipeName); var wasServerRunning = WasServerMutexOpen(serverMutexName); var timeout = wasServerRunning ? timeoutExistingProcess : timeoutNewProcess; if (wasServerRunning || tryCreateServerFunc(clientDir, pipeName, out var _, debug)) { pipeTask = Client.ConnectAsync(pipeName, TimeSpan.FromMilliseconds(timeout), cancellationToken); } } finally { if (holdsMutex) { clientMutex?.ReleaseMutex(); } clientMutex?.Dispose(); } if (pipeTask != null) { var client = await pipeTask.ConfigureAwait(false); if (client != null) { var request = ServerRequest.Create( serverPaths.WorkingDirectory, serverPaths.TempDirectory, arguments, keepAlive); return await TryProcessRequest(client, request, cancellationToken).ConfigureAwait(false); } } return new RejectedServerResponse(); } /// /// Try to process the request using the server. Returns a null-containing Task if a response /// from the server cannot be retrieved. /// private static async Task 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(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; } } // 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; } } } } /// /// This class provides simple properties for determining whether the current platform is Windows or Unix-based. /// We intentionally do not use System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(...) because /// it incorrectly reports 'true' for 'Windows' in desktop builds running on Unix-based platforms via Mono. /// internal static class PlatformInformation { public static bool IsWindows => Path.DirectorySeparatorChar == '\\'; public static bool IsUnix => Path.DirectorySeparatorChar == '/'; } }