Example #1
0
            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}.");
                    }
                }
            }
Example #2
0
        /// <summary>
        /// Check to ensure that the named pipe server we connected to is owned by the same
        /// user.
        /// </summary>
        private static bool CheckPipeConnectionOwnership(NamedPipeClientStream pipeStream)
        {
            try
            {
                if (PlatformInformation.IsWindows)
                {
                    using (var currentIdentity = WindowsIdentity.GetCurrent())
                    {
                        var currentOwner       = currentIdentity.Owner;
                        var remotePipeSecurity = GetPipeSecurity(pipeStream);
                        var remoteOwner        = remotePipeSecurity.GetOwner(typeof(SecurityIdentifier));

                        return(currentOwner.Equals(remoteOwner));
                    }
                }

                // We don't need to verify on non-windows as that will be taken care of by the
                // PipeOptions.CurrentUserOnly flag.
                return(false);
            }
            catch (Exception ex)
            {
                ServerLogger.LogException(ex, "Checking pipe connection");
                return(false);
            }
        }
Example #3
0
        /// <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);
            }
        }
Example #4
0
        // 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);
            }
        }
Example #5
0
            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);
                }
            }
Example #6
0
        private static DateTime?GetFileTimeStamp(string fullPath)
        {
            try
            {
                Debug.Assert(Path.IsPathRooted(fullPath));

                return(File.GetLastWriteTimeUtc(fullPath));
            }
            catch (Exception e)
            {
                // There are several exceptions that can occur here: NotSupportedException or PathTooLongException
                // for a bad path, UnauthorizedAccessException for access denied, etc. Rather than listing them all,
                // just catch all exceptions and log.
                ServerLogger.LogException(e, $"Error getting timestamp of file {fullPath}.");

                return(null);
            }
        }
Example #7
0
        // 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);
            }
        }
Example #8
0
        private static async Task <ServerResponse> RunOnServerCore(
            IList <string> arguments,
            ServerPaths serverPaths,
            string pipeName,
            string keepAlive,
            int?timeoutOverride,
            TryCreateServerCoreDelegate <string, string, int?, bool, bool> 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 <Client> 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());
        }
Example #9
0
        protected override Task <int> ExecuteCoreAsync()
        {
            // Make sure there's only one server with the same identity at a time.
            var   serverMutexName = MutexName.GetServerMutexName(Pipe.Value());
            Mutex serverMutex     = null;
            var   holdsMutex      = false;

            try
            {
                serverMutex = new Mutex(initiallyOwned: true, name: serverMutexName, 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

                Error.Write($"Server mutex creation failed. {ex.Message}");

                return(Task.FromResult(-1));
            }

            if (!holdsMutex)
            {
                // Another server is running, just exit.
                Error.Write("Another server already running...");
                return(Task.FromResult(1));
            }

            FileStream pidFileStream = null;

            try
            {
                try
                {
                    // Write the process and pipe information to a file in a well-known location.
                    pidFileStream = WritePidFile();
                }
                catch (Exception ex)
                {
                    // Something happened when trying to write to the pid file. Log and move on.
                    ServerLogger.LogException(ex, "Failed to create PID file.");
                }

                TimeSpan?keepAlive = null;
                if (KeepAlive.HasValue() && int.TryParse(KeepAlive.Value(), out var result))
                {
                    // Keep alive times are specified in seconds
                    keepAlive = TimeSpan.FromSeconds(result);
                }

                var host = ConnectionHost.Create(Pipe.Value());

                var compilerHost = CompilerHost.Create();
                ExecuteServerCore(host, compilerHost, Cancelled, eventBus: null, keepAlive: keepAlive);
            }
            finally
            {
                serverMutex.ReleaseMutex();
                serverMutex.Dispose();
                pidFileStream?.Close();
            }

            return(Task.FromResult(0));
        }
        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));
            }
        }