ServerLifecycleTest.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. using System.Diagnostics;
  4. using Moq;
  5. namespace Microsoft.NET.Sdk.Razor.Tool.Tests
  6. {
  7. public class ServerLifecycleTest
  8. {
  9. private static ServerRequest EmptyServerRequest => new(1, Array.Empty<RequestArgument>());
  10. private static ServerResponse EmptyServerResponse => new CompletedServerResponse(
  11. returnCode: 0,
  12. utf8output: false,
  13. output: string.Empty,
  14. error: string.Empty);
  15. [Fact]
  16. public void ServerStartup_MutexAlreadyAcquired_Fails()
  17. {
  18. // Arrange
  19. var pipeName = Guid.NewGuid().ToString("N");
  20. var mutexName = MutexName.GetServerMutexName(pipeName);
  21. var compilerHost = new Mock<CompilerHost>(MockBehavior.Strict);
  22. var host = new Mock<ConnectionHost>(MockBehavior.Strict);
  23. // Act & Assert
  24. using (var mutex = new Mutex(initiallyOwned: true, name: mutexName, createdNew: out var holdsMutex))
  25. {
  26. Assert.True(holdsMutex);
  27. try
  28. {
  29. var result = ServerUtilities.RunServer(pipeName, host.Object, compilerHost.Object);
  30. // Assert failure
  31. Assert.Equal(1, result);
  32. }
  33. finally
  34. {
  35. mutex.ReleaseMutex();
  36. }
  37. }
  38. }
  39. [Fact]
  40. public void ServerStartup_SuccessfullyAcquiredMutex()
  41. {
  42. // Arrange, Act & Assert
  43. var pipeName = Guid.NewGuid().ToString("N");
  44. var mutexName = MutexName.GetServerMutexName(pipeName);
  45. var compilerHost = new Mock<CompilerHost>(MockBehavior.Strict);
  46. var host = new Mock<ConnectionHost>(MockBehavior.Strict);
  47. #pragma warning disable xUnit1031
  48. host
  49. .Setup(x => x.WaitForConnectionAsync(It.IsAny<CancellationToken>()))
  50. .Returns(() =>
  51. {
  52. // Use a thread instead of Task to guarantee this code runs on a different
  53. // thread and we can validate the mutex state.
  54. var source = new TaskCompletionSource<bool>();
  55. var thread = new Thread(_ =>
  56. {
  57. Mutex mutex = null;
  58. try
  59. {
  60. Assert.True(Mutex.TryOpenExisting(mutexName, out mutex));
  61. Assert.False(mutex.WaitOne(millisecondsTimeout: 0));
  62. source.SetResult(true);
  63. }
  64. catch (Exception ex)
  65. {
  66. source.SetException(ex);
  67. throw;
  68. }
  69. finally
  70. {
  71. mutex?.Dispose();
  72. }
  73. });
  74. // Synchronously wait here. Don't returned a Task value because we need to
  75. // ensure the above check completes before the server hits a timeout and
  76. // releases the mutex.
  77. thread.Start();
  78. source.Task.Wait();
  79. return new TaskCompletionSource<Connection>().Task;
  80. });
  81. #pragma warning restore xUnit1031
  82. var result = ServerUtilities.RunServer(pipeName, host.Object, compilerHost.Object, keepAlive: TimeSpan.FromSeconds(1));
  83. Assert.Equal(0, result);
  84. }
  85. [Fact]
  86. public async Task ServerRunning_ShutdownRequest_processesSuccessfully()
  87. {
  88. // Arrange
  89. using (var serverData = ServerUtilities.CreateServer())
  90. {
  91. // Act
  92. var serverProcessId = await ServerUtilities.SendShutdown(serverData.PipeName);
  93. // Assert
  94. Assert.Equal(Process.GetCurrentProcess().Id, serverProcessId);
  95. await serverData.Verify(connections: 1, completed: 1);
  96. }
  97. }
  98. /// <summary>
  99. /// A shutdown request should not abort an existing compilation. It should be allowed to run to
  100. /// completion.
  101. /// </summary>
  102. [Fact]
  103. public async Task ServerRunning_ShutdownRequest_DoesNotAbortCompilation()
  104. {
  105. // Arrange
  106. var startCompilationSource = new TaskCompletionSource<bool>();
  107. var finishCompilationSource = new TaskCompletionSource<bool>();
  108. #pragma warning disable xUnit1031
  109. var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) =>
  110. {
  111. // At this point, the connection has been accepted and the compilation has started.
  112. startCompilationSource.SetResult(true);
  113. // We want this to keep running even after the shutdown is seen.
  114. finishCompilationSource.Task.Wait();
  115. return EmptyServerResponse;
  116. });
  117. #pragma warning restore xUnit1031
  118. using (var serverData = ServerUtilities.CreateServer(compilerHost: host))
  119. {
  120. var compileTask = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest);
  121. // Wait for the request to go through and trigger compilation.
  122. await startCompilationSource.Task;
  123. // Act
  124. // The compilation is now in progress, send the shutdown.
  125. await ServerUtilities.SendShutdown(serverData.PipeName);
  126. Assert.False(compileTask.IsCompleted);
  127. // Now let the task complete.
  128. finishCompilationSource.SetResult(true);
  129. // Assert
  130. var response = await compileTask;
  131. Assert.Equal(ServerResponse.ResponseType.Completed, response.Type);
  132. Assert.Equal(0, ((CompletedServerResponse)response).ReturnCode);
  133. await serverData.Verify(connections: 2, completed: 2);
  134. }
  135. }
  136. /// <summary>
  137. /// Multiple clients should be able to send shutdown requests to the server.
  138. /// </summary>
  139. [Fact]
  140. public async Task ServerRunning_MultipleShutdownRequests_HandlesSuccessfully()
  141. {
  142. // Arrange
  143. var startCompilationSource = new TaskCompletionSource<bool>();
  144. var finishCompilationSource = new TaskCompletionSource<bool>();
  145. #pragma warning disable xUnit1031
  146. var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) =>
  147. {
  148. // At this point, the connection has been accepted and the compilation has started.
  149. startCompilationSource.SetResult(true);
  150. // We want this to keep running even after the shutdown is seen.
  151. finishCompilationSource.Task.Wait();
  152. return EmptyServerResponse;
  153. });
  154. #pragma warning restore xUnit1031
  155. using (var serverData = ServerUtilities.CreateServer(compilerHost: host))
  156. {
  157. var compileTask = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest);
  158. // Wait for the request to go through and trigger compilation.
  159. await startCompilationSource.Task;
  160. // Act
  161. for (var i = 0; i < 10; i++)
  162. {
  163. // The compilation is now in progress, send the shutdown.
  164. var processId = await ServerUtilities.SendShutdown(serverData.PipeName);
  165. Assert.Equal(Process.GetCurrentProcess().Id, processId);
  166. Assert.False(compileTask.IsCompleted);
  167. }
  168. // Now let the task complete.
  169. finishCompilationSource.SetResult(true);
  170. // Assert
  171. var response = await compileTask;
  172. Assert.Equal(ServerResponse.ResponseType.Completed, response.Type);
  173. Assert.Equal(0, ((CompletedServerResponse)response).ReturnCode);
  174. await serverData.Verify(connections: 11, completed: 11);
  175. }
  176. }
  177. // https://github.com/aspnet/Razor/issues/1991
  178. [WindowsOnlyFact]
  179. public async Task ServerRunning_CancelCompilation_CancelsSuccessfully()
  180. {
  181. // Arrange
  182. const int requestCount = 5;
  183. var count = 0;
  184. var completionSource = new TaskCompletionSource<bool>();
  185. var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) =>
  186. {
  187. if (Interlocked.Increment(ref count) == requestCount)
  188. {
  189. completionSource.SetResult(true);
  190. }
  191. ct.WaitHandle.WaitOne();
  192. return new RejectedServerResponse();
  193. });
  194. var semaphore = new SemaphoreSlim(1);
  195. Action<object, EventArgs> onListening = (s, e) =>
  196. {
  197. semaphore.Release();
  198. };
  199. using (var serverData = ServerUtilities.CreateServer(compilerHost: host, onListening: onListening))
  200. {
  201. // Send all the requests.
  202. var clients = new List<Client>();
  203. for (var i = 0; i < requestCount; i++)
  204. {
  205. // Wait for the server to start listening.
  206. await semaphore.WaitAsync(TimeSpan.FromMinutes(1));
  207. var client = await Client.ConnectAsync(serverData.PipeName, timeout: null, cancellationToken: default);
  208. await EmptyServerRequest.WriteAsync(client.Stream);
  209. clients.Add(client);
  210. }
  211. // Act
  212. // Wait until all of the connections are being processed by the server.
  213. await completionSource.Task;
  214. // Now cancel
  215. var stats = await serverData.CancelAndCompleteAsync();
  216. // Assert
  217. Assert.Equal(requestCount, stats.Connections);
  218. Assert.Equal(requestCount, count);
  219. // Read the server response to each client.
  220. foreach (var client in clients)
  221. {
  222. var task = ServerResponse.ReadAsync(client.Stream);
  223. // We expect this to throw because the stream is already closed.
  224. await Assert.ThrowsAnyAsync<IOException>(() => task);
  225. client.Dispose();
  226. }
  227. }
  228. }
  229. private static TestableCompilerHost CreateCompilerHost(Action<TestableCompilerHost> configureCompilerHost = null)
  230. {
  231. var compilerHost = new TestableCompilerHost();
  232. configureCompilerHost?.Invoke(compilerHost);
  233. return compilerHost;
  234. }
  235. private class TestableCompilerHost : CompilerHost
  236. {
  237. internal Func<ServerRequest, CancellationToken, ServerResponse> ExecuteFunc;
  238. public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken)
  239. {
  240. if (ExecuteFunc != null)
  241. {
  242. return ExecuteFunc(request, cancellationToken);
  243. }
  244. return EmptyServerResponse;
  245. }
  246. }
  247. }
  248. }