diff --git a/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs b/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs index 7ba222918425c1..00a392a2bec071 100644 --- a/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs +++ b/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs @@ -305,6 +305,8 @@ internal static bool ValidServiceName(string serviceName) /// /// Disposes of the resources (other than memory ) used by /// the . + /// This is called from when all + /// services in the process have entered the SERVICE_STOPPED state. /// protected override void Dispose(bool disposing) { diff --git a/src/libraries/System.ServiceProcess.ServiceController/tests/ServiceBaseTests.cs b/src/libraries/System.ServiceProcess.ServiceController/tests/ServiceBaseTests.cs index efe9733aa51100..da535fafea8e79 100644 --- a/src/libraries/System.ServiceProcess.ServiceController/tests/ServiceBaseTests.cs +++ b/src/libraries/System.ServiceProcess.ServiceController/tests/ServiceBaseTests.cs @@ -84,17 +84,21 @@ public void TestOnStartThenStop() public void TestOnStartWithArgsThenStop() { ServiceController controller = ConnectToServer(); - controller.Stop(); Assert.Equal((int)PipeMessageByteCode.Stop, _testService.GetByte()); controller.WaitForStatus(ServiceControllerStatus.Stopped); controller.Start(new string[] { "StartWithArguments", "a", "b", "c" }); + + // Start created a new TestService; dispose of our client stream and reconnect to it _testService.Client = null; _testService.Client.Connect(); - Assert.Equal((int)(PipeMessageByteCode.Connected), _testService.GetByte()); - Assert.Equal((int)(PipeMessageByteCode.Start), _testService.GetByte()); + // Test service does not mutually synchronize Connected and Start messages + var bytes = new byte[] { _testService.GetByte(), _testService.GetByte() }; + Assert.Contains((byte)PipeMessageByteCode.Connected, bytes); + Assert.Contains((byte)PipeMessageByteCode.Start, bytes); + controller.WaitForStatus(ServiceControllerStatus.Running); controller.Stop(); @@ -202,8 +206,10 @@ public void PropagateExceptionFromOnStart() private ServiceController ConnectToServer() { + TestServiceProvider.DebugTrace("ServiceBaseTests.ConnectToServer: connecting"); _testService.Client.Connect(connectionTimeout); Assert.Equal((int)PipeMessageByteCode.Connected, _testService.GetByte()); + TestServiceProvider.DebugTrace("ServiceBaseTests.ConnectToServer: received connect byte"); ServiceController controller = new ServiceController(_testService.TestServiceName); AssertExpectedProperties(controller); diff --git a/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/Program.cs b/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/Program.cs index 8773e4e3acfbeb..abff7d1c7c7425 100644 --- a/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/Program.cs +++ b/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/Program.cs @@ -1,12 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Threading; + namespace System.ServiceProcess.Tests { public class Program { static int Main(string[] args) { + Thread.CurrentThread.Name = "Test Service"; if (args.Length == 1 || args.Length == 2) { TestService testService; diff --git a/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestService.cs b/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestService.cs index a22f7b967594f6..5d3d9bc808f704 100644 --- a/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestService.cs +++ b/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestService.cs @@ -9,6 +9,11 @@ namespace System.ServiceProcess.Tests { public class TestService : ServiceBase { + // To view tracing, use DbgView from sysinternals.com; + // run it elevated, check "Capture>Global Win32" and "Capture>Win32", + // and filter to just messages beginning with "##" + internal const bool DebugTracing = false; + private bool _disposed; private Task _waitClientConnect; private NamedPipeServerStream _serverStream; @@ -16,6 +21,7 @@ public class TestService : ServiceBase public TestService(string serviceName, Exception throwException = null) { + DebugTrace("TestService " + ServiceName + ": Ctor"); this.ServiceName = serviceName; // Enable all the events @@ -27,10 +33,11 @@ public TestService(string serviceName, Exception throwException = null) this.CanHandleSessionChangeEvent = false; this.CanHandlePowerEvent = false; this._exception = throwException; - this._serverStream = new NamedPipeServerStream(serviceName); _waitClientConnect = this._serverStream.WaitForConnectionAsync(); - _waitClientConnect.ContinueWith((t) => WriteStreamAsync(PipeMessageByteCode.Connected)); + _waitClientConnect = _waitClientConnect.ContinueWith(_ => DebugTrace("TestService " + ServiceName + ": Connected")); + _waitClientConnect.ContinueWith(t => WriteStreamAsync(PipeMessageByteCode.Connected)); + DebugTrace("TestService " + ServiceName + ": Ctor completed"); } protected override void OnContinue() @@ -72,6 +79,7 @@ protected override void OnShutdown() protected override void OnStart(string[] args) { + DebugTrace("TestService " + ServiceName + ": OnStart"); base.OnStart(args); if (_exception != null) { @@ -89,41 +97,52 @@ protected override void OnStart(string[] args) protected override void OnStop() { + DebugTrace("TestService " + ServiceName + ": OnStop"); base.OnStop(); WriteStreamAsync(PipeMessageByteCode.Stop).Wait(); } public async Task WriteStreamAsync(PipeMessageByteCode code, int command = 0) { - if (_waitClientConnect.IsCompleted) - { - Task writeCompleted; - const int WriteTimeout = 60000; - if (code == PipeMessageByteCode.OnCustomCommand) - { - writeCompleted = _serverStream.WriteAsync(new byte[] { (byte)command }, 0, 1); - } - else - { - writeCompleted = _serverStream.WriteAsync(new byte[] { (byte)code }, 0, 1); - } - await writeCompleted.TimeoutAfter(WriteTimeout).ConfigureAwait(false); - } - else + DebugTrace("TestService " + ServiceName + ": WriteStreamAsync writing " + code.ToString()); + + const int WriteTimeout = 60000; + var toWrite = (code == PipeMessageByteCode.OnCustomCommand) ? new byte[] { (byte)command } : new byte[] { (byte)code }; + + // Wait for the client connection before writing to the pipe. + // Exception: if it's a Stop message, it may be because the test has completed and we're cleaning up the test service so there's no client at all. + // Tests that verify "Stop" itself should ensure the client connection has completed before calling stop, by waiting on some other message from the pipe first. + if (code != PipeMessageByteCode.Stop) { - // We get here if the service is getting torn down before a client ever connected. - // some tests do this. + await _waitClientConnect; } + await _serverStream.WriteAsync(toWrite, 0, 1).TimeoutAfter(WriteTimeout).ConfigureAwait(false); + DebugTrace("TestService " + ServiceName + ": WriteStreamAsync completed"); } + /// + /// This is called from when all services in the process + /// have entered the SERVICE_STOPPED state. It disposes the named pipe stream. + /// protected override void Dispose(bool disposing) { if (!_disposed) { + DebugTrace("TestService " + ServiceName + ": Disposing"); _serverStream.Dispose(); _disposed = true; base.Dispose(); } } + + internal static void DebugTrace(string message) + { + if (DebugTracing) + { +#pragma warning disable CS0162 // unreachable code + Debug.WriteLine("## " + message); +#pragma warning restore + } + } } } diff --git a/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestServiceInstaller.cs b/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestServiceInstaller.cs index 10d941807aec8f..daad7d2af0ac23 100644 --- a/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestServiceInstaller.cs +++ b/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestServiceInstaller.cs @@ -102,6 +102,7 @@ public unsafe void Install() { if (svc.Status != ServiceControllerStatus.Running) { + TestService.DebugTrace("TestServiceInstaller: instructing ServiceController to Start service " + ServiceName); svc.Start(); if (!ServiceName.StartsWith("PropagateExceptionFromOnStart")) svc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(120)); @@ -124,7 +125,6 @@ public void RemoveService() // Meantime we still want this service to get deleted, so we'll go ahead and call // DeleteService, which will schedule it to get deleted on reboot. // We won't catch the exception: we do want the test to fail. - DeleteService(); ServiceName = null; @@ -141,6 +141,7 @@ private void StopService() { try { + TestService.DebugTrace("TestServiceInstaller: instructing ServiceController to Stop service " + ServiceName); svc.Stop(); } catch (InvalidOperationException) @@ -172,6 +173,7 @@ private void DeleteService() if (serviceHandle.IsInvalid) throw new Win32Exception($"Could not find service '{ServiceName}'"); + TestService.DebugTrace("TestServiceInstaller: instructing ServiceController to Delete service " + ServiceName); if (!Interop.Advapi32.DeleteService(serviceHandle)) { throw new Win32Exception($"Could not delete service '{ServiceName}'"); diff --git a/src/libraries/System.ServiceProcess.ServiceController/tests/TestServiceProvider.cs b/src/libraries/System.ServiceProcess.ServiceController/tests/TestServiceProvider.cs index c875d7ea895016..1be86a61785dc9 100644 --- a/src/libraries/System.ServiceProcess.ServiceController/tests/TestServiceProvider.cs +++ b/src/libraries/System.ServiceProcess.ServiceController/tests/TestServiceProvider.cs @@ -10,6 +10,11 @@ namespace System.ServiceProcess.Tests { internal sealed class TestServiceProvider { + // To view tracing, use DbgView from sysinternals.com; + // run it elevated, check "Capture>Global Win32" and "Capture>Win32", + // and filter to just messages beginning with "##" + internal const bool DebugTracing = false; + private const int readTimeout = 60000; private static readonly Lazy s_runningWithElevatedPrivileges = new Lazy( @@ -28,6 +33,7 @@ public NamedPipeClientStream Client { if (_client == null) { + DebugTrace("TestServiceProvider: Creating client stream"); _client = new NamedPipeClientStream(".", TestServiceName, PipeDirection.In); } return _client; @@ -36,10 +42,12 @@ public NamedPipeClientStream Client { if (value == null) { + DebugTrace("TestServiceProvider: Disposing client stream"); _client.Dispose(); _client = null; } } + } public readonly string TestServiceAssembly = typeof(TestService).Assembly.Location; @@ -125,6 +133,7 @@ public void DeleteTestServices() { if (_client != null) { + DebugTrace("TestServiceProvider: Disposing client stream in Dispose()"); _client.Dispose(); _client = null; } @@ -143,5 +152,15 @@ public void DeleteTestServices() } } } + + internal static void DebugTrace(string message) + { + if (DebugTracing) + { +#pragma warning disable CS0162 // unreachable code + Debug.WriteLine("## " + message); +#pragma warning restore + } + } } }