#pragma region CPL License /* Nuclex Native Framework Copyright (C) 2002-2023 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #pragma endregion // CPL License #ifndef NUCLEX_SUPPORT_THREADING_PROCESS_H #define NUCLEX_SUPPORT_THREADING_PROCESS_H #include "Nuclex/Support/Config.h" // Currently, the process wrapper only has implementations for Linux and Windows // // The Linux version may be fully or nearly Posix-compatible, so feel free to // remove this check and give it a try. #if defined(NUCLEX_SUPPORT_LINUX) || defined(NUCLEX_SUPPORT_WINDOWS) #include "Nuclex/Support/Events/ConcurrentEvent.h" #include // for std::chrono::milliseconds #include // for std::vector #include // for std::string namespace Nuclex { namespace Support { namespace Threading { // ------------------------------------------------------------------------------------------- // /// Wraps an external executable running as an independent process /// /// /// This is a convenient helper you can use to run external programs. It will /// deal with the differences between platforms in finding the target executable, /// creating the new process, redirecting its stdin, stdout and stderr streams /// and checking in on the process' status. /// /// /// When specifying an executable name without an absolute path, the directory /// containing the running application will be searched first. This allows you to /// easily call supporting executables that ship with your application, such as /// shader compilers, updaters and launchers. /// /// /// For external processes that generate output (such as a compiler), it is very /// important to keep pumping the output streams by calling /// regularly. Otherwise, child process' will /// eventually fill the buffers of its stdout and stderr redirection pipes and /// hang on an std::cout or printf() call waiting for buffer space to free up. /// /// class NUCLEX_SUPPORT_TYPE Process { /// Returns the directory in which the running executable resides /// The directory holding the currently running executable /// /// /// The returned path is the application's executable directory, guaranteed to /// end with the platform's native directory separator character. If you directly /// append a filename to the returned string, you get a valid, absolute path to /// any file stored in the same directory as your application's executable. /// /// /// Do note that on Unix-like platforms it is usually not appropriate to store /// data and configuration files in the application directory, unless your /// application is installed in the '/opt' directory (but hardcoding such /// a requirement would get in the way of a package manager installing your /// application in '/usr/bin' and its data files in '/usr/share'). /// /// public: NUCLEX_SUPPORT_API static std::string GetExecutableDirectory(); /// Event that is fired whenever the process writes to stdout public: Nuclex::Support::Events::ConcurrentEvent< void(const char *, std::size_t) > StdOut; /// Event that is fired whenever the process writes to stderr public: Nuclex::Support::Events::ConcurrentEvent< void(const char *, std::size_t) > StdErr; /// Initializes a new process without starting it /// /// Executable that should be run, optionally including its path /// /// /// Whether to intercept the child process' stderr. Setting this to false will render /// the StdErr event inoperable and cause all stderr output to land in the calling /// parent process' stderr. /// /// /// Whether to intercept the child process' stdout. Setting this to false will render /// the StdOut event inoperable and cause all stdout output to land in the calling /// parent process' stdout. /// /// /// /// If the specified executable name doesn't contain a path (or is specified with /// a relative path), the path is interpreted as relative to the directory in which /// the running application's executable resides. /// /// /// Should the specified executable not be found that way, the normal search rules of /// the underlying operating system apply, i.e. the PATH environment variable is used /// in addition to any documented OS-specific search rules and ordering. /// /// /// The aforementioned executable search will not be attempted in the first place if /// you specify an absolute path, so for helper executables that ship with your /// application, specifying the full path is the fastest and safest approach. /// /// public: NUCLEX_SUPPORT_API Process( const std::string &executablePath, bool interceptStdErr = true, bool interceptStdOut = true ); /// Kills the external process and waits until it is gone public: NUCLEX_SUPPORT_API ~Process(); /// Sets the working directory the child process will start in /// /// Initial working directory the child process will use. Set to an empty string /// to use the current working directory of the parent process /// /// /// The working directory starts out as empty, meaning it will be left to /// the operating system what working directory to use. Usually that means /// whatever directory the parent process was in when the child process started. /// public: NUCLEX_SUPPORT_API void SetWorkingDirectory( const std::string &newWorkingDirectory ) { this->workingDirectory = newWorkingDirectory; } /// /// Starts the external process, passing the specified command-line arguments along /// /// Arguments that will be passed to the external process /// /// Whether to make the first argument the path to the executable. Most applications /// expect this and some even require it (like Linux' busybox, which decides to act as /// different programs depending on the name it's invoked through). /// /// /// There's a major difference to how arguments are passed to a process between Linux /// and Windows. On Linux, arguments are an array of strings, allowing spaces to be /// passed along. On Windows, the arguments become a single string, meaning that there /// is no way to distinguish between an argument containing a space and two arguments. /// public: NUCLEX_SUPPORT_API void Start( const std::vector &arguments = std::vector(), bool prependExecutableName = true ); /// Checks whether the process is still running /// True if the process was still running, false otherwise public: NUCLEX_SUPPORT_API bool IsRunning() const; /// Waits for the process to exit normally /// /// Time the method will wait for the process to exit. /// /// /// True if the process exited within the allotted time, false if it is still running. /// /// /// If the process does exit (and this method returned 'true'), you still have to /// call to check the exit code of the process. The Join() method /// will return instantaneously in that case. /// public: NUCLEX_SUPPORT_API bool Wait( std::chrono::milliseconds patience = std::chrono::milliseconds(30000) ) const; /// Waits for the process to exit normally and returns its exit code /// /// Time the method will wait for the process to exit. If the process does no exit within /// this time, an exception will be thrown. /// /// /// The exit code returned by the process (most programs adhere to returning 0 if /// everything went well and 1 if there was a problem). /// public: NUCLEX_SUPPORT_API int Join( std::chrono::milliseconds patience = std::chrono::milliseconds(30000) ); /// Attempts to terminate the external process /// /// Time given to the process to respond to a request to gracefully terminate. /// If zero, the process is forcefully killed immediately. /// /// /// This will first attempt to gracefully exit the running process (by sending it /// a SIGTERM signal or closing its main window). If this doesn't result in the process /// terminating within the a grace period, this method will attempt to terminate /// the process forcefully via either SIGKILL or by aborting its process. /// public: NUCLEX_SUPPORT_API void Kill( std::chrono::milliseconds patience = std::chrono::milliseconds(5000) ); /// Sends input to the running process' stdin /// Characters that will be sent to the process' stdin /// Number of characters /// The number of bytes that have been written to the process' stdin /// /// If you fill the buffer of the process' stdin pipe, it may not be possible to /// write more data to stdin until the process has read from stdin. /// public: NUCLEX_SUPPORT_API std::size_t Write( const char *characters, std::size_t characterCount ); /// Fetches data from the stdout and stderr streams /// True if data was pulled from either stdout or stderr /// /// /// If console output of the external process is redirected into pipes, these pipes /// have a limited buffer. Once the buffer is full, the external process will block /// until the pipe's buffer has been emptied. /// /// /// Because of that, it's very important to call this method regularly, especially if /// the child process is generating a lot of output. Not doing so can cause the child /// process to wait forever in a printf() or std::cout call. /// /// /// The provided Wait() and Join() methods will automatically flush the pipe's output /// buffers adequately, but if you just let the instance linger in the background, /// be sure to have some mechanism that calls PumpOutputStreams() regularly. /// /// /// The and events will be synchronously /// invoked from the thread calling this method. You can use the return value to decide /// whether to immediately check for more data or whether to pause for a few milliseconds /// to give the CPU idle cycles when there's no output being generated. /// /// public: NUCLEX_SUPPORT_API bool PumpOutputStreams() const; // Useful? I'd like to keep this class tight and focused rather then turning // it into a general-purpose grabbag for all your child process needs. //public: std::any GetNativeProcessId() const; // Useful? This would be easy to provide, but I'd rather expose such things // purely through the Nuclex.Storage.Native library. //public: static std::string SearchExternalExecutable(const std::string &executableName); /// Path to the executable this process instance is launching private: std::string executablePath; /// Working directory the child process will start in private: std::string workingDirectory; /// Pipe buffer (uses round-robin to flush stdout and stderr) private: mutable std::vector buffer; /// Whether the stdout of the child process is intercepted private: bool interceptStdOut; /// Whether the stderr of the child process is intercepted private: bool interceptStdErr; /// Structure to hold platform dependent process and file handles private: struct PlatformDependentImplementationData; /// Accesses the platform dependent implementation data container /// A reference to the platform dependent implementation data private: const PlatformDependentImplementationData &getImplementationData() const; /// Accesses the platform dependent implementation data container /// A reference to the platform dependent implementation data private: PlatformDependentImplementationData &getImplementationData(); private: union { /// Platform dependent process and file handles used for the process PlatformDependentImplementationData *implementationData; /// Used to hold the platform dependent implementation data if it fits /// /// Small performance / memory fragmentation improvement. /// This avoids a micro-allocation for the implenmentation data structure in most cases. /// #if defined(NUCLEX_SUPPORT_WINDOWS) unsigned char implementationDataBuffer[32]; #else // Posix and Linux unsigned char implementationDataBuffer[24]; #endif }; }; // ------------------------------------------------------------------------------------------- // }}} // namespace Nuclex::Support::Threading #endif // defined(NUCLEX_SUPPORT_LINUX) || defined(NUCLEX_SUPPORT_WINDOWS) #endif // NUCLEX_SUPPORT_THREADING_THREAD_H