// MonoGame - Copyright (C) The MonoGame Team // This file is subject to the terms and conditions defined in // file 'LICENSE.txt', which is part of this source code package. using System; using System.Diagnostics; using System.IO; using System.Threading; using MonoGame.Utilities; namespace Microsoft.Xna.Framework.Content.Pipeline { /// /// Helper to run an external tool installed in the system. Useful for when /// we don't want to package the tool ourselves (ffmpeg) or it's provided /// by a third party (console manufacturer). /// internal class ExternalTool { public static int Run(string command, string arguments) { string stdout, stderr; var result = Run(command, arguments, out stdout, out stderr); if (result < 0) throw new Exception(string.Format("{0} returned exit code {1}", command, result)); return result; } public static int Run(string command, string arguments, out string stdout, out string stderr, string stdin = null) { // This particular case is likely to be the most common and thus // warrants its own specific error message rather than falling // back to a general exception from Process.Start() var fullPath = FindCommand(command); if (string.IsNullOrEmpty(fullPath)) throw new Exception(string.Format("Couldn't locate external tool '{0}'.", command)); // We can't reference ref or out parameters from within // lambdas (for the thread functions), so we have to store // the data in a temporary variable and then assign these // variables to the out parameters. var stdoutTemp = string.Empty; var stderrTemp = string.Empty; var processInfo = new ProcessStartInfo { Arguments = arguments, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false, FileName = fullPath, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, }; EnsureExecutable(fullPath); using (var process = new Process()) { process.StartInfo = processInfo; process.Start(); // We have to run these in threads, because using ReadToEnd // on one stream can deadlock if the other stream's buffer is // full. var stdoutThread = new Thread(new ThreadStart(() => { var memory = new MemoryStream(); process.StandardOutput.BaseStream.CopyTo(memory); var bytes = new byte[memory.Position]; memory.Seek(0, SeekOrigin.Begin); memory.Read(bytes, 0, bytes.Length); stdoutTemp = System.Text.Encoding.ASCII.GetString(bytes); })); var stderrThread = new Thread(new ThreadStart(() => { var memory = new MemoryStream(); process.StandardError.BaseStream.CopyTo(memory); var bytes = new byte[memory.Position]; memory.Seek(0, SeekOrigin.Begin); memory.Read(bytes, 0, bytes.Length); stderrTemp = System.Text.Encoding.ASCII.GetString(bytes); })); stdoutThread.Start(); stderrThread.Start(); if (stdin != null) { process.StandardInput.Write(System.Text.Encoding.ASCII.GetBytes(stdin)); } // Make sure interactive prompts don't block. process.StandardInput.Close(); process.WaitForExit(); stdoutThread.Join(); stderrThread.Join(); stdout = stdoutTemp; stderr = stderrTemp; return process.ExitCode; } } /// /// Returns the fully-qualified path for a command, searching the system path if necessary. /// /// /// It's apparently necessary to use the full path when running on some systems. /// private static string FindCommand(string command) { // Expand any environment variables. command = Environment.ExpandEnvironmentVariables(command); // If we have a full path just pass it through. if (File.Exists(command)) return command; // We don't have a full path, so try running through the system path to find it. var paths = AppDomain.CurrentDomain.BaseDirectory + Path.PathSeparator + Environment.GetEnvironmentVariable("PATH"); var justTheName = Path.GetFileName(command); foreach (var path in paths.Split(Path.PathSeparator)) { var fullName = Path.Combine(path, justTheName); if (File.Exists(fullName)) return fullName; if (CurrentPlatform.OS == OS.Windows) { var fullExeName = string.Concat(fullName, ".exe"); if (File.Exists(fullExeName)) return fullExeName; } } return null; } /// /// Ensures the specified executable has the executable bit set. If the /// executable doesn't have the executable bit set on Linux or Mac OS, then /// Mono will refuse to execute it. /// /// The full path to the executable. private static void EnsureExecutable(string path) { #if LINUX || MACOS if (path == "/bin/bash") return; try { var p = Process.Start("chmod", "u+x '" + path + "'"); p.WaitForExit(); } catch { // This platform may not have chmod in the path, in which case we can't // do anything reasonable here. } #endif } /// /// Safely deletes the file if it exists. /// /// The path to the file to delete. public static void DeleteFile(string filePath) { try { File.Delete(filePath); } catch (Exception) { } } } }