using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using OpenTap.Diagnostic;
namespace OpenTap
{
///
/// This is an abstraction for running child processes with support for elevation.
/// It executes a test step (which can have child test steps) in a new process
/// It supports subscribing to log events from the child process, and forwarding the logs directly.
///
class SubProcessHost
{
public bool ForwardLogs { get; set; }
public string LogHeader { get; set; } = "";
public bool Unlocked { get; set; } = false;
public HashSet MutedSources { get; } = new HashSet();
public static bool IsAdmin()
{
if (OperatingSystem.Current == OperatingSystem.Windows)
{
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
else // assume UNIX
{
// id -u should print '0' if running as sudo or the current user is root
var pInfo = new ProcessStartInfo()
{
FileName = "id",
Arguments = "-u",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
};
using (var p = Process.Start(pInfo))
{
if (p == null) return false;
var output = p.StandardOutput.ReadToEnd().Trim();
if (int.TryParse(output, out var id) && id == 0)
return true;
return false;
}
}
}
private static readonly TraceSource log = Log.CreateSource(nameof(SubProcessHost));
internal Process LastProcessHandle;
private static readonly object StdoutLock = new object();
private static bool stdoutSuspended;
private static TextWriter originalOut;
private static StringWriter tmpOut;
private static void SuspendStdout()
{
lock (StdoutLock)
{
if (stdoutSuspended) return;
originalOut = Console.Out;
tmpOut = new StringWriter();
Console.SetOut(tmpOut);
originalOut.Flush();
stdoutSuspended = true;
}
}
private static void ResumeStdout()
{
lock (StdoutLock)
{
if (stdoutSuspended == false) return;
// Restore stdout after the server has connected
Console.SetOut(originalOut);
var lines = tmpOut.ToString()
.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
Console.WriteLine(line);
}
stdoutSuspended = false;
tmpOut.Dispose();
tmpOut = null;
}
}
public Verdict Run(ITestStep step, bool elevate, CancellationToken token)
{
var plan = new TestPlan();
plan.ChildTestSteps.Add(step);
try
{
return Run(plan, elevate, token);
}
catch (Win32Exception ex)
{
// This happens when the UAC dialog is cancelled. It should be treated as an OperationCanceledException.
if (ex.Message.Contains("The operation was canceled by the user"))
throw new OperationCanceledException(ex.Message);
throw;
}
}
public Verdict Run(TestPlan step, bool elevate, CancellationToken token)
{
var dotnet = ExecutorClient.Dotnet;
var tapDll = Path.Combine(ExecutorClient.ExeDir, "tap.dll");
var handle = Guid.NewGuid().ToString();
var pInfo = new ProcessStartInfo(dotnet)
{
Arguments = $"\"{tapDll}\" {nameof(ProcessCliAction)} --PipeHandle \"{handle}\"",
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
if (elevate)
{
if (OperatingSystem.Current == OperatingSystem.Linux)
{
// -E preserves environment variables
pInfo.Arguments = $"-E \"{pInfo.FileName}\" {pInfo.Arguments}";
pInfo.FileName = "sudo";
if (SudoHelper.IsSudoAuthenticated() == false)
if (SudoHelper.Authenticate() == false)
throw new Exception($"User failed to authenticate as sudo.");
}
else
{
pInfo.Verb = "runas";
pInfo.UseShellExecute = true;
pInfo.RedirectStandardOutput = false;
pInfo.RedirectStandardError = false;
}
}
SuspendStdout();
using var p = Process.Start(pInfo);
LastProcessHandle = p ?? throw new Exception($"Failed to spawn process.");
// Ensure the process is cleaned up
TapThread.Current.AbortToken.Register(() =>
{
if (p.HasExited) return;
try
{
// process.Kill may throw if it has already exited.
p.Kill();
}
catch (Exception ex)
{
log.Warning("Caught exception when killing process. {0}", ex.Message);
}
});
try
{
var server = new NamedPipeServerStream(handle, PipeDirection.InOut, 1);
try
{
server.WaitForConnectionAsync(token).Wait(token);
}
catch (OperationCanceledException)
{
throw new OperationCanceledException($"Process cancelled by the user.");
}
// Resume stdout after the server has connected as we now know the application has launched
ResumeStdout();
server.WriteMessage(step);
while (server.IsConnected && p.HasExited == false)
{
if (token.IsCancellationRequested)
throw new OperationCanceledException($"Process cancelled by the user.");
if (server.TryReadMessage(out var events) && ForwardLogs)
{
if (string.IsNullOrWhiteSpace(LogHeader) == false)
{
for(int i = 0; i < events.Length; i++)
events[i].Message = LogHeader + ": " + events[i].Message;
}
var _evt = events;
if (MutedSources.Any())
{
_evt = events.Where(e => !MutedSources.Contains(e.Source)).ToArray();
}
_evt.ForEach(((ILogContext2)Log.Context).AddEvent);
}
}
var processExitTask = Task.Run(() => p.WaitForExit(), token);
var tokenCancelledTask = Task.Run(() => token.WaitHandle.WaitOne(), token);
Task.WaitAny(processExitTask, tokenCancelledTask);
if (token.IsCancellationRequested)
{
throw new OperationCanceledException($"Process cancelled by the user.");
}
return (Verdict) p.ExitCode;
}
finally
{
ResumeStdout();
if (p.HasExited == false)
{
p.Kill();
}
}
}
}
}