ExampleRunner.cs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. using System.Diagnostics;
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Reflection;
  4. using System.Text.Json;
  5. using System.Text.RegularExpressions;
  6. namespace Terminal.Gui.Examples;
  7. /// <summary>
  8. /// Provides methods for running example applications in various execution modes.
  9. /// </summary>
  10. public static class ExampleRunner
  11. {
  12. /// <summary>
  13. /// Runs an example with the specified context.
  14. /// </summary>
  15. /// <param name="example">The example information.</param>
  16. /// <param name="context">The execution context.</param>
  17. /// <returns>The result of running the example.</returns>
  18. [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
  19. [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
  20. public static ExampleResult Run (ExampleInfo example, ExampleContext context)
  21. {
  22. return context.Mode == ExecutionMode.InProcess
  23. ? RunInProcess (example, context)
  24. : RunOutOfProcess (example, context);
  25. }
  26. private static ExampleMetrics? ExtractMetricsFromOutput (string output)
  27. {
  28. // Look for the metrics marker in the output
  29. Match match = Regex.Match (output, @"###TERMGUI_METRICS:(.+?)###");
  30. if (!match.Success)
  31. {
  32. return null;
  33. }
  34. try
  35. {
  36. return JsonSerializer.Deserialize<ExampleMetrics> (match.Groups [1].Value);
  37. }
  38. catch
  39. {
  40. return null;
  41. }
  42. }
  43. [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
  44. [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
  45. private static ExampleResult RunInProcess (ExampleInfo example, ExampleContext context)
  46. {
  47. Environment.SetEnvironmentVariable (
  48. ExampleContext.ENVIRONMENT_VARIABLE_NAME,
  49. context.ToJson ());
  50. try
  51. {
  52. Assembly asm = Assembly.LoadFrom (example.AssemblyPath);
  53. MethodInfo? entryPoint = asm.EntryPoint;
  54. if (entryPoint is null)
  55. {
  56. return new ()
  57. {
  58. Success = false,
  59. ErrorMessage = "Assembly does not have an entry point"
  60. };
  61. }
  62. ParameterInfo [] parameters = entryPoint.GetParameters ();
  63. Task executionTask = Task.Run (() =>
  64. {
  65. object? result = null;
  66. if (parameters.Length == 0)
  67. {
  68. result = entryPoint.Invoke (null, null);
  69. }
  70. else if (parameters.Length == 1 && parameters [0].ParameterType == typeof (string []))
  71. {
  72. result = entryPoint.Invoke (null, [Array.Empty<string> ()]);
  73. }
  74. else
  75. {
  76. throw new InvalidOperationException ("Entry point has unsupported signature");
  77. }
  78. // If entry point returns Task, wait for it
  79. if (result is Task task)
  80. {
  81. task.GetAwaiter ().GetResult ();
  82. }
  83. });
  84. bool completed = executionTask.Wait (context.TimeoutMs);
  85. if (!completed)
  86. {
  87. // reset terminal
  88. Console.Clear ();
  89. return new ()
  90. {
  91. Success = false,
  92. TimedOut = true
  93. };
  94. }
  95. if (executionTask.Exception is { })
  96. {
  97. throw executionTask.Exception.GetBaseException ();
  98. }
  99. return new ()
  100. {
  101. Success = true
  102. };
  103. }
  104. catch (Exception ex)
  105. {
  106. return new ()
  107. {
  108. Success = false,
  109. ErrorMessage = ex.ToString ()
  110. };
  111. }
  112. finally
  113. {
  114. Environment.SetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME, null);
  115. }
  116. }
  117. private static ExampleResult RunOutOfProcess (ExampleInfo example, ExampleContext context)
  118. {
  119. ProcessStartInfo psi = new ()
  120. {
  121. FileName = "dotnet",
  122. Arguments = $"\"{example.AssemblyPath}\"",
  123. UseShellExecute = false,
  124. RedirectStandardOutput = true,
  125. RedirectStandardError = true,
  126. CreateNoWindow = true
  127. };
  128. psi.Environment [ExampleContext.ENVIRONMENT_VARIABLE_NAME] = context.ToJson ();
  129. using Process? process = Process.Start (psi);
  130. if (process is null)
  131. {
  132. return new ()
  133. {
  134. Success = false,
  135. ErrorMessage = "Failed to start process"
  136. };
  137. }
  138. bool exited = process.WaitForExit (context.TimeoutMs);
  139. string stdout = process.StandardOutput.ReadToEnd ();
  140. string stderr = process.StandardError.ReadToEnd ();
  141. if (!exited)
  142. {
  143. try
  144. {
  145. const bool KILL_ENTIRE_PROCESS_TREE = true;
  146. process.Kill (KILL_ENTIRE_PROCESS_TREE);
  147. }
  148. catch
  149. {
  150. // Ignore errors killing the process
  151. }
  152. return new ()
  153. {
  154. Success = false,
  155. TimedOut = true,
  156. StandardOutput = stdout,
  157. StandardError = stderr
  158. };
  159. }
  160. ExampleMetrics? metrics = ExtractMetricsFromOutput (stdout);
  161. return new ()
  162. {
  163. Success = process.ExitCode == 0,
  164. ExitCode = process.ExitCode,
  165. StandardOutput = stdout,
  166. StandardError = stderr,
  167. Metrics = metrics
  168. };
  169. }
  170. }