src/common/IO/Platform.cs (295 lines of code) (raw):

// -------------------------------------------------------------------------------------------- // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Security.Principal; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Microsoft.BridgeToKubernetes.Common.Utilities; using static Microsoft.BridgeToKubernetes.Common.Constants; namespace Microsoft.BridgeToKubernetes.Common.IO { /// <summary> /// NetStandardPlatform Platform. /// </summary> internal class Platform : IPlatform { [DllImport("libc", SetLastError = true)] private static extern int waitpid(int pid, out int status, int options); public bool IsWindows => OperatingSystem.IsWindows(); public bool IsOSX => OperatingSystem.IsMacOS(); public bool IsLinux => OperatingSystem.IsLinux(); public async Task<(int exitCode, string userName)> DetermineCurrentUserWithRetriesAsync(CancellationToken cancellationToken) { int exitCode = 0; string userName = string.Empty; await WebUtilities.RetryUntilTimeWithWaitAsync((i) => { (exitCode, userName) = DetermineCurrentUser(); return Task.FromResult(!string.IsNullOrWhiteSpace(userName)); }, maxWaitTime: TimeSpan.FromSeconds(3), waitInterval: TimeSpan.FromMilliseconds(100), cancellationToken: cancellationToken); return (exitCode, userName); } /// <summary> /// <see cref="IPlatform.Execute(string, string, Action{string}, IDictionary{string, string}, TimeSpan, CancellationToken, out string)"/> /// </summary> public int Execute( string executable, string command, Action<string> logCallback, IDictionary<string, string> envVariables, TimeSpan timeout, CancellationToken cancellationToken, out string output) { cancellationToken.ThrowIfCancellationRequested(); var combinedOutput = new StringBuilder(); Action<string> callback = line => { combinedOutput.AppendLine(line); logCallback?.Invoke(line); }; int result = Execute(executable, command, callback, callback, envVariables, timeout, cancellationToken, out string tmp1, out string tmp2); output = combinedOutput.ToString(); return result; } /// <summary> /// <see cref="IPlatform.Execute(string, string, Action{string}, Action{string}, IDictionary{string, string}, TimeSpan, CancellationToken, out string, out string)"/> /// </summary> public int Execute( string executable, string command, Action<string> stdOutCallback, Action<string> stdErrCallback, IDictionary<string, string> envVariables, TimeSpan timeout, CancellationToken cancellationToken, out string stdOutOutput, out string stdErrOutput) { cancellationToken.ThrowIfCancellationRequested(); ProcessStartInfo psi = new ProcessStartInfo() { FileName = executable, Arguments = command, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true }; var process = new ProcessEx(psi); if (envVariables != null) { foreach (KeyValuePair<string, string> env in envVariables) { process.StartInfo.EnvironmentVariables[env.Key] = env.Value; } } using (var outputWaitCountdown = new CountdownEvent(2)) // 2 for stdout and stderr { StringBuilder stdOutLines = new StringBuilder(); StringBuilder stdErrLines = new StringBuilder(); object stdOutLock = new object(); object stdErrLock = new object(); void outputHandler(object sender, DataReceivedEventArgs e) { if (e.Data != null) { lock (stdOutLock) { stdOutLines.AppendLine(e.Data); } stdOutCallback?.Invoke(e.Data); } else { try { process.OutputDataReceived -= outputHandler; } catch { } try { // Output data has finished outputWaitCountdown.Signal(); } catch (ObjectDisposedException) { } } }; void errorHandler(object sender, DataReceivedEventArgs e) { if (e.Data != null) { lock (stdErrLock) { stdErrLines.AppendLine(e.Data); } stdErrCallback?.Invoke(e.Data); } else { try { process.ErrorDataReceived -= errorHandler; } catch { } try { // Error data has finished outputWaitCountdown.Signal(); } catch (ObjectDisposedException) { } } } process.OutputDataReceived += outputHandler; process.ErrorDataReceived += errorHandler; Action killProcess = () => { try { if (!process.HasExited) { process.Kill(); process.Dispose(); } } catch (Exception e) { stdErrLines.AppendLine(e.ToString()); } }; int exitCode; using (cancellationToken.Register(killProcess)) { process.Start(); process.BeginErrorReadLine(); process.BeginOutputReadLine(); int timeoutMs = timeout.TotalMilliseconds > int.MaxValue ? int.MaxValue : (int)timeout.TotalMilliseconds; exitCode = WaitForProcessExit(process, timeoutMs, stdOutCallback, stdErrCallback); } try { // Wait for all output to flush // The process should already be exited at this point, so don't wait too long before giving up if (!outputWaitCountdown.Wait(TimeSpan.FromSeconds(5))) { throw new IOException("Timed out waiting for process output to flush"); } } finally { lock (stdOutLock) { stdOutOutput = stdOutLines.ToString(); } lock (stdErrLock) { stdErrOutput = stdErrLines.ToString(); } process.Dispose(); } return exitCode; } } public (int exitCode, string output) ExecuteAndReturnOutput( string command, string arguments, TimeSpan timeout, Action<string> stdOutCallback, Action<string> stdErrCallback, string workingDirectory = null, string processInput = null) { ProcessStartInfo psi = new ProcessStartInfo() { UseShellExecute = false, RedirectStandardError = true, RedirectStandardOutput = true, FileName = command, Arguments = arguments }; if (!string.IsNullOrWhiteSpace(workingDirectory)) { psi.WorkingDirectory = workingDirectory; } if (!string.IsNullOrWhiteSpace(processInput)) { psi.RedirectStandardInput = true; } StringBuilder sb = new StringBuilder(); var proc = new ProcessEx(psi); proc.OutputDataReceived += (sender, e) => { sb.AppendLine(e.Data); }; proc.ErrorDataReceived += (sender, e) => { sb.AppendLine(e.Data); }; proc.Start(); if (!string.IsNullOrWhiteSpace(processInput)) { proc.StandardInput.WriteLine(processInput); proc.StandardInput.Close(); } proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); proc.WaitForExit(); var exitCode = WaitForProcessExit(proc, timeoutMs: timeout.Milliseconds, stdOutCallback: stdOutCallback, stdErrCallback: stdErrCallback); return (exitCode, sb.ToString()); } // NOTE: Do not use this method as-is on MacOS. Use WaitForExit(), and/or redirect output as well. When we do these things, the process exits when it is done. Otherwise, // process.WaitForExit(timeoutMs) returns true immediately, before the timeout has elapsed. // For more info: https://github.com/dotnet/runtime/issues/32456 private int WaitForProcessExit(IProcessEx process, int timeoutMs, Action<string> stdOutCallback, Action<string> stdErrCallback) { stdOutCallback?.Invoke($"Waiting for process {process.Id}"); int exitCode; if (!process.WaitForExit(timeoutMs)) { // Process did not terminate within the timeout if (stdErrCallback != null) { stdErrCallback($"Killing process. Timeout: {timeoutMs} ms reached"); } exitCode = (int)ExitCode.Timeout; process.Kill(); } else { if (stdOutCallback != null) { stdOutCallback($"Process has exited with exit code {process.ExitCode}"); } exitCode = process.ExitCode; } return exitCode; } public IProcessEx CreateProcess(ProcessStartInfo psi) => new ProcessEx(psi); public void KillProcess(int processId) => Process.GetProcessById(processId).Kill(); private Version GetOSXVersion(Action<string> stdOutCallback, Action<string> stdErrCallback) { stdOutCallback?.Invoke("Getting OSX Version"); Version osxVersion = new Version(10, 14); // By default, assume OSX Majave 10.14 try { // in OSX, /System/Library/CoreServices/SystemVersion.plist is a XML file a ProductVersion property: // <key>ProductVersion</key> // <string>10.11.1</string> // https://www.cyberciti.biz/faq/mac-osx-find-tell-operating-system-version-from-bash-prompt/ string sysVerFile = @"/System/Library/CoreServices/SystemVersion.plist"; if (File.Exists(sysVerFile)) { var content = File.ReadAllText(sysVerFile); var xdoc = XDocument.Parse(content); var dictElement = xdoc.Root.Element("dict"); bool productVersionFound = false; if (dictElement != null) { foreach (var k in dictElement.Elements()) { if (StringComparer.OrdinalIgnoreCase.Equals(k.Value, "ProductVersion")) { productVersionFound = true; } else if (productVersionFound) { stdOutCallback?.Invoke($"OSX Version: {k.Value}"); osxVersion = new Version(k.Value); break; } } } } } catch (Exception ex) { stdErrCallback?.Invoke($"Error {ex.Message} trying to get OSX version. Assume {osxVersion}"); } stdOutCallback?.Invoke($"Found version {osxVersion}"); return osxVersion; } private (int exitCode, string userName) DetermineCurrentUser() { if (this.IsWindows) { return (0, WindowsIdentity.GetCurrent().Name); } // For Mac & Linux, run "whoami" to determine current user var whoami = this.IsOSX ? "/usr/bin/whoami" : "whoami"; (var exitCode, var output) = this.ExecuteAndReturnOutput(whoami, arguments: null, timeout: TimeSpan.FromSeconds(1), stdOutCallback: null, stdErrCallback: null); return (exitCode, output.Trim()); } } }