private static RuntimePlatform CalculateRuntimePlatform() { #if !XPLAT return(RuntimePlatform.Windows); #else const PlatformID MonoOldUnix = (PlatformID)128; switch (Environment.OSVersion.Platform) { case PlatformID.Win32NT: return(RuntimePlatform.Windows); case PlatformID.Unix: case MonoOldUnix: // Mono returns PlatformID.Unix on OSX for compatibility return(PlatformUtilities.GetUnixVariant()); case PlatformID.MacOSX: return(RuntimePlatform.MacOSX); default: return(RuntimePlatform.Unknown); } #endif }
public override void InitStreams(LaunchOptions options, out StreamReader reader, out StreamWriter writer) { LocalLaunchOptions localOptions = (LocalLaunchOptions)options; string miDebuggerDir = System.IO.Path.GetDirectoryName(localOptions.MIDebuggerPath); Process proc = new Process(); proc.StartInfo.FileName = localOptions.MIDebuggerPath; proc.StartInfo.Arguments = "--interpreter=mi"; proc.StartInfo.WorkingDirectory = miDebuggerDir; // On Windows, GDB locally requires that the directory be on the PATH, being the working directory isn't good enough if (PlatformUtilities.IsWindows() && options.DebuggerMIMode == MIMode.Gdb) { string path = proc.StartInfo.GetEnvironmentVariable("PATH"); path = (string.IsNullOrEmpty(path) ? miDebuggerDir : path + ";" + miDebuggerDir); proc.StartInfo.SetEnvironmentVariable("PATH", path); } foreach (EnvironmentEntry entry in localOptions.Environment) { proc.StartInfo.SetEnvironmentVariable(entry.Name, entry.Value); } InitProcess(proc, out reader, out writer); }
/// <summary> /// Launch a new terminal, spin up a new bash shell, cd to the working dir, execute a tty command to get the shell tty and store it. /// Start the debugger in mi mode setting the tty to the terminal defined earlier and redirect stdin/stdout /// to the correct pipes. After the debugger exits, cleanup the FIFOs. This is done using the trap command to add a /// signal handler for SIGHUP on the console (executing the two rm commands) /// </summary> /// <param name="debuggeeDir">Path to the debuggee directory</param> /// <param name="dbgStdInName">File where the stdin for the debugger process is redirected to</param> /// <param name="dbgStdOutName">File where the stdout for the debugger process is redirected to</param> /// <param name="pidFifo">File where the debugger pid is written to</param> /// <param name="dbgCmdScript">Script file to pass the debugger launch command</param> /// <param name="debuggerCmd">Command to the debugger</param> /// <param name="debuggerArgs">MIDebugger arguments</param> /// <returns></returns> internal static string LaunchLocalDebuggerCommand( string debuggeeDir, string dbgStdInName, string dbgStdOutName, string pidFifo, string dbgCmdScript, string debuggerCmd, string debuggerArgs) { // On OSX, 'wait' will return once there is a status change from the launched process rather than for it to exit, so // we need to use 'fg' there. This works as our bash prompt is launched through apple script rather than 'bash -c'. // On Linux, fg will fail with 'no job control' because our commands are being executed through 'bash -c', and // bash doesn't support fg in this mode, so we need to use 'wait' there. string waitForCompletionCommand = PlatformUtilities.IsOSX() ? "fg > /dev/null; " : "wait $pid; "; // echo the shell pid so that we can monitor it // Change to the current working directory // find the tty // enable monitor on the session // set the trap command to remove the fifos // execute the debugger command in the background // Clear the output of executing a process in the background: [job number] pid // echo and wait the debugger pid to know whether we need to fake an exit by the debugger return(FormattableString.Invariant($"echo $$ > {pidFifo} ; cd \"{debuggeeDir}\" ; DbgTerm=`tty` ; set -o monitor ; trap 'rm \"{dbgStdInName}\" \"{dbgStdOutName}\" \"{pidFifo}\" \"{dbgCmdScript}\"' EXIT ; {debuggerCmd} {debuggerArgs} --tty=$DbgTerm < \"{dbgStdInName}\" > \"{dbgStdOutName}\" & clear; pid=$! ; echo $pid > \"{pidFifo}\" ; {waitForCompletionCommand}")); }
/// <summary> /// Launch a new terminal, spin up a new bash shell, cd to the working dir, execute a tty command to get the shell tty and store it. /// Start the debugger in mi mode setting the tty to the terminal defined earlier and redirect stdin/stdout /// to the correct pipes. After the debugger exits, cleanup the FIFOs. This is done using the trap command to add a /// signal handler for SIGHUP on the console (executing the two rm commands) /// </summary> /// <param name="debuggeeDir">Path to the debuggee directory</param> /// <param name="dbgStdInName">File where the stdin for the debugger process is redirected to</param> /// <param name="dbgStdOutName">File where the stdout for the debugger process is redirected to</param> /// <param name="pidFifo">File where the debugger pid is written to</param> /// <param name="debuggerCmd">Command to the debugger</param> /// <returns></returns> internal static string LaunchLocalDebuggerCommand( string debuggeeDir, string dbgStdInName, string dbgStdOutName, string pidFifo, string debuggerCmd) { // On OSX, 'wait' will return once there is a status change from the launched process rather than for it to exit, so // we need to use 'fg' there. This works as our bash prompt is launched through apple script rather than 'bash -c'. // On Linux, fg will fail with 'no job control' because our commands are being executed through 'bash -c', and // bash doesn't support fg in this mode, so we need to use 'wait' there. string waitForCompletionCommand = PlatformUtilities.IsOSX() ? "fg > /dev/null; " : "wait $pid; "; return(string.Format(CultureInfo.InvariantCulture, // echo the shell pid so that we can monitor it "echo $$ > {3}; " + "cd {0}; " + "DbgTerm=`tty`; " + "trap 'rm {1} {2} {3}' EXIT; " + "{4} --interpreter=mi --tty=$DbgTerm < {1} > {2} & " + // Clear the output of executing a process in the background: [job number] pid "clear; " + // echo and wait the debugger pid to know whether // we need to fake an exit by the debugger "pid=$! ; " + "echo $pid > {3}; " + "{5}", debuggeeDir, /* 0 */ dbgStdInName, /* 1 */ dbgStdOutName, /* 2 */ pidFifo, /* 3 */ debuggerCmd, /* 4 */ waitForCompletionCommand /* 5 */ )); }
internal static void KillProcessTree(Process p) { bool isLinux = PlatformUtilities.IsLinux(); bool isOSX = PlatformUtilities.IsOSX(); if (isLinux || isOSX) { // On linux run 'ps -x -o "%p %P"' (similarly on Mac), which generates a list of the process ids (%p) and parent process ids (%P). // Using this list, issue a 'kill' command for each child process. Kill the children (recursively) to eliminate // the entire process tree rooted at p. Process ps = new Process(); ps.StartInfo.FileName = "/bin/ps"; ps.StartInfo.Arguments = isLinux ? "-x -o \"%p %P\"" : "-x -o \"pid ppid\""; ps.StartInfo.RedirectStandardOutput = true; ps.StartInfo.UseShellExecute = false; ps.Start(); string line; List <Tuple <int, int> > processAndParent = new List <Tuple <int, int> >(); char[] whitespace = new char[] { ' ', '\t' }; while ((line = ps.StandardOutput.ReadLine()) != null) { line = line.Trim(); int id, pid; if (Int32.TryParse(line.Substring(0, line.IndexOfAny(whitespace)), NumberStyles.Integer, CultureInfo.InvariantCulture, out id) && Int32.TryParse(line.Substring(line.IndexOfAny(whitespace)).Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out pid)) { processAndParent.Add(new Tuple <int, int>(id, pid)); } } KillChildren(processAndParent, p.Id); } }
public override void InitStreams(LaunchOptions options, out StreamReader reader, out StreamWriter writer) { LocalLaunchOptions localOptions = (LocalLaunchOptions)options; string miDebuggerDir = System.IO.Path.GetDirectoryName(localOptions.MIDebuggerPath); Process proc = new Process(); proc.StartInfo.FileName = localOptions.MIDebuggerPath; proc.StartInfo.Arguments = localOptions.GetMiDebuggerArgs(); // LLDB has the -environment-cd mi command that is used to set the working dir for gdb/clrdbg, but it doesn't work. // So, set lldb's working dir to the user's requested folder before launch. proc.StartInfo.WorkingDirectory = options.DebuggerMIMode == MIMode.Lldb ? options.WorkingDirectory : miDebuggerDir; // On Windows, GDB locally requires that the directory be on the PATH, being the working directory isn't good enough if (PlatformUtilities.IsWindows() && options.DebuggerMIMode == MIMode.Gdb) { string path = proc.StartInfo.GetEnvironmentVariable("PATH"); path = (string.IsNullOrEmpty(path) ? miDebuggerDir : path + ";" + miDebuggerDir); proc.StartInfo.SetEnvironmentVariable("PATH", path); } // Only pass the environment to launch clrdbg. For other modes, there are commands that set the environment variables // directly for the debuggee. if (options.DebuggerMIMode == MIMode.Clrdbg) { foreach (EnvironmentEntry entry in localOptions.Environment) { proc.StartInfo.SetEnvironmentVariable(entry.Name, entry.Value); } } InitProcess(proc, out reader, out writer); }
public ProcessMonitor(int processId) { if (!PlatformUtilities.IsLinux() && !PlatformUtilities.IsOSX()) { throw new NotImplementedException(); } _processId = processId; }
private void LaunchSuccess(int?pid) { if (_pidReader != null) { int shellPid; Task <string> readShellPidTask = _pidReader.ReadLineAsync(); if (readShellPidTask.Wait(TimeSpan.FromSeconds(10))) { shellPid = int.Parse(readShellPidTask.Result, CultureInfo.InvariantCulture); // Used for testing Logger?.WriteLine(string.Concat("ShellPid=", shellPid)); } else { // Something is wrong because we didn't get the pid of shell ForceDisposeStreamReader(_pidReader); Close(); throw new TimeoutException(string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_RunInTerminalFailure, MICoreResources.Error_TimeoutWaitingForConnection)); } if (!PlatformUtilities.IsWindows()) { _shellProcessMonitor = new ProcessMonitor(shellPid); _shellProcessMonitor.ProcessExited += ShellExited; _shellProcessMonitor.Start(); } else { Process shellProcess = Process.GetProcessById(shellPid); shellProcess.EnableRaisingEvents = true; shellProcess.Exited += ShellExited; } Task <string> readDebuggerPidTask = _pidReader.ReadLineAsync(); try { readDebuggerPidTask.Wait(_streamReadPidCancellationTokenSource.Token); _debuggerPid = int.Parse(readDebuggerPidTask.Result, CultureInfo.InvariantCulture); } catch (OperationCanceledException) { // Something is wrong because we didn't get the pid of the debugger ForceDisposeStreamReader(_pidReader); Close(); throw new OperationCanceledException(string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_RunInTerminalFailure, MICoreResources.Error_UnableToEstablishConnectionToLauncher)); } } if (debuggerPidCallback != null) { debuggerPidCallback(_debuggerPid); } }
public override bool UseExternalConsoleForLocalLaunch(LocalLaunchOptions localLaunchOptions) { // NOTE: On Linux, there are issues if we try to have GDB launch the process as a child of VS // code -- it will cause a deadlock during debuggee launch. So we always use the external console // unless we are in a scenario where the debuggee will not be a child process. if (PlatformUtilities.IsLinux()) { return(String.IsNullOrEmpty(localLaunchOptions.MIDebuggerServerAddress) && !localLaunchOptions.IsCoreDump); } else { return(base.UseExternalConsoleForLocalLaunch(localLaunchOptions)); } }
internal bool PreparePath(string path, bool useUnixFormat, out string pathMI) { bool requiresQuotes = false; path = path.Trim(); if (useUnixFormat) // convert directory separators { path = PlatformUtilities.WindowsPathToUnixPath(path); } if (path.IndexOf(' ') != -1) // path contains spaces. Convert to c-string format { path = path.Replace(@"\", @"\\"); // escape any backslashes in the path requiresQuotes = true; // parameter containing the name will need to be quoted } pathMI = path; return(requiresQuotes); }
public static TerminalLauncher MakeTerminal(string title, string initScript, ReadOnlyCollection <EnvironmentEntry> environment) { TerminalLauncher terminal = null; if (PlatformUtilities.IsLinux()) { terminal = new LinuxTerminalLauncher(title, initScript, environment); } else if (PlatformUtilities.IsOSX()) { terminal = new MacTerminalLauncher(title, initScript, environment); } else { Debug.Fail("Cannot make a terminal for non Linux or OS X."); throw new InvalidOperationException(); } return(terminal); }
internal static string GetDebuggerCommand(LocalLaunchOptions localOptions) { string quotedDebuggerPath = String.Format(CultureInfo.InvariantCulture, "\"{0}\"", localOptions.MIDebuggerPath); if (PlatformUtilities.IsLinux()) { string debuggerPathCorrectElevation = quotedDebuggerPath; string prompt = string.Empty; // If running as root, make sure the new console is also root. bool isRoot = UnixNativeMethods.GetEUid() == 0; // If the system doesn't allow a non-root process to attach to another process, try to run GDB as root if (localOptions.ProcessId.HasValue && !isRoot && UnixUtilities.GetRequiresRootAttach(localOptions.DebuggerMIMode)) { prompt = String.Format(CultureInfo.CurrentCulture, "echo -n '{0}'; read yn; if [ \"$yn\" != 'y' ] && [ \"$yn\" != 'Y' ] ; then exit 1; fi; ", MICoreResources.Warn_AttachAsRootProcess); // Prefer pkexec for a nice graphical prompt, but fall back to sudo if it's not available if (File.Exists(UnixUtilities.PKExecPath)) { debuggerPathCorrectElevation = String.Concat(UnixUtilities.PKExecPath, " ", debuggerPathCorrectElevation); } else if (File.Exists(UnixUtilities.SudoPath)) { debuggerPathCorrectElevation = String.Concat(UnixUtilities.SudoPath, " ", debuggerPathCorrectElevation); } else { Debug.Fail("Root required to attach, but no means of elevating available!"); } } return(String.Concat(prompt, debuggerPathCorrectElevation)); } else { return(quotedDebuggerPath); } }
public static bool IsBinarySigned(string filePath) { if (!PlatformUtilities.IsOSX()) { throw new NotImplementedException(); } Process p = new Process { StartInfo = { CreateNoWindow = true, UseShellExecute = true, FileName = CodeSignPath, Arguments = "--display " + filePath } }; p.Start(); p.WaitForExit(); return(p.ExitCode == 0); }
public static bool IsBinarySigned(string filePath, Logger logger) { if (!PlatformUtilities.IsOSX()) { throw new NotImplementedException(); } Process p = new Process { StartInfo = { CreateNoWindow = true, UseShellExecute = false, FileName = CodeSignPath, Arguments = "--display " + filePath, RedirectStandardOutput = true, RedirectStandardError = true } }; p.OutputDataReceived += (sender, e) => { OutputNonEmptyString(e.Data, "codeSign-stdout: ", logger); }; p.ErrorDataReceived += (sender, e) => { OutputNonEmptyString(e.Data, "codeSign-stderr: ", logger); }; p.Start(); p.BeginOutputReadLine(); p.BeginErrorReadLine(); p.WaitForExit(); return(p.ExitCode == 0); }
public override void InitStreams(LaunchOptions options, out StreamReader reader, out StreamWriter writer) { LocalLaunchOptions localOptions = (LocalLaunchOptions)options; string miDebuggerDir = System.IO.Path.GetDirectoryName(localOptions.MIDebuggerPath); Process proc = new Process(); proc.StartInfo.FileName = localOptions.MIDebuggerPath; proc.StartInfo.Arguments = localOptions.GetMiDebuggerArgs(); // LLDB has the -environment-cd mi command that is used to set the working dir for gdb, but it doesn't work. // So, set lldb's working dir to the user's requested folder before launch. proc.StartInfo.WorkingDirectory = options.DebuggerMIMode == MIMode.Lldb ? options.WorkingDirectory : miDebuggerDir; // On Windows, GDB locally requires that the directory be on the PATH, being the working directory isn't good enough if (PlatformUtilities.IsWindows() && options.DebuggerMIMode == MIMode.Gdb) { string path = proc.StartInfo.GetEnvironmentVariable("PATH"); path = (string.IsNullOrEmpty(path) ? miDebuggerDir : path + ";" + miDebuggerDir); proc.StartInfo.SetEnvironmentVariable("PATH", path); } // Allow to execute custom commands before launching debugger. // For ex., instructing GDB not to break for certain signals if (options.DebuggerMIMode == MIMode.Gdb && !string.IsNullOrWhiteSpace(options.WorkingDirectory)) { var gdbInitFile = Path.Combine(options.WorkingDirectory, ".gdbinit"); if (File.Exists(gdbInitFile)) { proc.StartInfo.Arguments += " -x \"" + gdbInitFile + "\""; } } InitProcess(proc, out reader, out writer); }
public override async void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop waitLoop = null) { LocalLaunchOptions localOptions = options as LocalLaunchOptions; Encoding encNoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); string commandPipeName; string outputPipeName; string pidPipeName; List <string> cmdArgs = new List <string>(); string windowtitle = FormattableString.Invariant($"cppdbg: {Path.GetFileName(options.ExePath)}"); if (PlatformUtilities.IsWindows()) { // Create Windows Named pipes commandPipeName = Utilities.GetMIEngineTemporaryFilename("In"); outputPipeName = Utilities.GetMIEngineTemporaryFilename("Out"); pidPipeName = Utilities.GetMIEngineTemporaryFilename("Pid"); string errorPipeName = Utilities.GetMIEngineTemporaryFilename("Error"); NamedPipeServerStream inputToDebugger = new NamedPipeServerStream(commandPipeName, PipeDirection.Out, 1, PipeTransmissionMode.Byte); NamedPipeServerStream outputFromDebugger = new NamedPipeServerStream(outputPipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte); NamedPipeServerStream errorFromDebugger = new NamedPipeServerStream(errorPipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte); NamedPipeServerStream pidPipe = new NamedPipeServerStream(pidPipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte); _pidReader = new StreamReader(pidPipe, encNoBom, false, UnixUtilities.StreamBufferSize); string thisModulePath = typeof(RunInTerminalTransport).GetTypeInfo().Assembly.ManifestModule.FullyQualifiedName; string launchCommand = Path.Combine(Path.GetDirectoryName(thisModulePath), "WindowsDebugLauncher.exe"); if (!File.Exists(launchCommand)) { string errorMessage = string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_InternalFileMissing, launchCommand); transportCallback.OnStdErrorLine(errorMessage); transportCallback.OnDebuggerProcessExit(null); return; } cmdArgs.Add(launchCommand); cmdArgs.Add("--stdin=" + commandPipeName); cmdArgs.Add("--stdout=" + outputPipeName); cmdArgs.Add("--stderr=" + errorPipeName); cmdArgs.Add("--pid=" + pidPipeName); cmdArgs.Add("--dbgExe=" + localOptions.MIDebuggerPath); cmdArgs.Add(localOptions.GetMiDebuggerArgs()); _waitForConnection = Task.WhenAll( inputToDebugger.WaitForConnectionAsync(), outputFromDebugger.WaitForConnectionAsync(), errorFromDebugger.WaitForConnectionAsync(), pidPipe.WaitForConnectionAsync()); _commandStream = new StreamWriter(inputToDebugger, encNoBom); _outputStream = new StreamReader(outputFromDebugger, encNoBom, false, UnixUtilities.StreamBufferSize); _errorStream = new StreamReader(errorFromDebugger, encNoBom, false, UnixUtilities.StreamBufferSize); } else { // Do Linux style pipes commandPipeName = UnixUtilities.MakeFifo(identifier: "In", logger: logger); outputPipeName = UnixUtilities.MakeFifo(identifier: "Out", logger: logger); pidPipeName = UnixUtilities.MakeFifo(identifier: "Pid", logger: logger); // Create filestreams FileStream stdInStream = new FileStream(commandPipeName, FileMode.Open); FileStream stdOutStream = new FileStream(outputPipeName, FileMode.Open); _pidReader = new StreamReader(new FileStream(pidPipeName, FileMode.Open), encNoBom, false, UnixUtilities.StreamBufferSize); string debuggerCmd = UnixUtilities.GetDebuggerCommand(localOptions); // Default working directory is next to the app string debuggeeDir; if (Path.IsPathRooted(options.ExePath) && File.Exists(options.ExePath)) { debuggeeDir = Path.GetDirectoryName(options.ExePath); } else { // If we don't know where the app is, default to HOME, and if we somehow can't get that, go with the root directory. debuggeeDir = Environment.GetEnvironmentVariable("HOME"); if (string.IsNullOrEmpty(debuggeeDir)) { debuggeeDir = "/"; } } string dbgCmdScript = Path.Combine(Path.GetTempPath(), Utilities.GetMIEngineTemporaryFilename(identifier: "Cmd")); string launchDebuggerCommand = UnixUtilities.LaunchLocalDebuggerCommand( debuggeeDir, commandPipeName, outputPipeName, pidPipeName, dbgCmdScript, debuggerCmd, localOptions.GetMiDebuggerArgs()); logger?.WriteTextBlock("DbgCmd:", launchDebuggerCommand); using (FileStream dbgCmdStream = new FileStream(dbgCmdScript, FileMode.CreateNew)) using (StreamWriter dbgCmdWriter = new StreamWriter(dbgCmdStream, encNoBom) { AutoFlush = true }) { dbgCmdWriter.WriteLine("#!/bin/sh"); dbgCmdWriter.Write(launchDebuggerCommand); dbgCmdWriter.Flush(); } if (PlatformUtilities.IsOSX()) { string osxLaunchScript = GetOSXLaunchScript(); // Call osascript with a path to the AppleScript. The apple script takes 2 parameters: a title for the terminal and the launch script. cmdArgs.Add("/usr/bin/osascript"); cmdArgs.Add(osxLaunchScript); cmdArgs.Add(FormattableString.Invariant($"\"{windowtitle}\"")); cmdArgs.Add(FormattableString.Invariant($"sh {dbgCmdScript} ;")); // needs a semicolon because this command is running through the launchscript. } else { cmdArgs.Add("sh"); cmdArgs.Add(dbgCmdScript); } _outputStream = new StreamReader(stdOutStream, encNoBom, false, UnixUtilities.StreamBufferSize); _commandStream = new StreamWriter(stdInStream, encNoBom); } RunInTerminalLauncher launcher = new RunInTerminalLauncher(windowtitle, localOptions.Environment); launcher.Launch( cmdArgs, localOptions.UseExternalConsole, LaunchSuccess, (error) => { transportCallback.OnStdErrorLine(error); throw new InvalidOperationException(error); }, logger); logger?.WriteLine("Wait for connection completion."); if (_waitForConnection != null) { // Add a timeout for waiting for connection - 20 seconds Task waitOrTimeout = Task.WhenAny(_waitForConnection, Task.Delay(20000)); await waitOrTimeout; if (waitOrTimeout.Status != TaskStatus.RanToCompletion) { string errorMessage = String.Format(CultureInfo.CurrentCulture, MICoreResources.Error_DebuggerInitializeFailed_NoStdErr, "WindowsDebugLauncher.exe"); transportCallback.OnStdErrorLine(errorMessage); transportCallback.OnDebuggerProcessExit(null); return; } } base.Init(transportCallback, options, logger, waitLoop); }