diff options
author | Devtools Arcadia <arcadia-devtools@yandex-team.ru> | 2022-02-07 18:08:42 +0300 |
---|---|---|
committer | Devtools Arcadia <arcadia-devtools@mous.vla.yp-c.yandex.net> | 2022-02-07 18:08:42 +0300 |
commit | 1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch) | |
tree | e26c9fed0de5d9873cce7e00bc214573dc2195b7 /util/system/shellcommand.h | |
download | ydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz |
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'util/system/shellcommand.h')
-rw-r--r-- | util/system/shellcommand.h | 485 |
1 files changed, 485 insertions, 0 deletions
diff --git a/util/system/shellcommand.h b/util/system/shellcommand.h new file mode 100644 index 0000000000..8730627fe5 --- /dev/null +++ b/util/system/shellcommand.h @@ -0,0 +1,485 @@ +#pragma once + +#include <util/generic/noncopyable.h> +#include <util/generic/string.h> +#include <util/generic/list.h> +#include <util/generic/hash.h> +#include <util/generic/strbuf.h> +#include <util/generic/maybe.h> +#include <util/stream/input.h> +#include <util/stream/output.h> +#include "file.h" +#include "getpid.h" +#include "thread.h" +#include "mutex.h" +#include <sys/types.h> + +class TShellCommandOptions { +public: + struct TUserOptions { + TString Name; +#if defined(_win_) + TString Password; +#endif +#if defined(_unix_) + /** + * Run child process with the user supplementary groups. + * If true, the user supplementary groups will be set in the child process upon exec(). + * If false, the supplementary groups of the parent process will be used. + */ + bool UseUserGroups = false; +#endif + }; + + enum EHandleMode { + HANDLE_INHERIT, + HANDLE_PIPE, + HANDLE_STREAM + }; + +public: + inline TShellCommandOptions() noexcept + : ClearSignalMask(false) + , CloseAllFdsOnExec(false) + , AsyncMode(false) + , PollDelayMs(DefaultSyncPollDelay) + , UseShell(true) + , QuoteArguments(true) + , DetachSession(true) + , CloseStreams(false) + , ShouldCloseInput(true) + , InputMode(HANDLE_INHERIT) + , OutputMode(HANDLE_STREAM) + , ErrorMode(HANDLE_STREAM) + , InputStream(nullptr) + , OutputStream(nullptr) + , ErrorStream(nullptr) + , Nice(0) + , FuncAfterFork(std::function<void()>()) + { + } + + inline TShellCommandOptions& SetNice(int value) noexcept { + Nice = value; + + return *this; + } + + /** + * @brief clear signal mask from parent process. If true, child process + * clears the signal mask inherited from the parent process; otherwise + * child process retains the signal mask of the parent process. + * + * @param clearSignalMask true if child process should clear signal mask + * @note in default child process inherits signal mask. + * @return self + */ + inline TShellCommandOptions& SetClearSignalMask(bool clearSignalMask) { + ClearSignalMask = clearSignalMask; + return *this; + } + + /** + * @brief set close-on-exec mode. If true, all file descriptors + * from the parent process, except stdin, stdout, stderr, will be closed + * in the child process upon exec(). + * + * @param closeAllFdsOnExec true if close-on-exec mode is needed + * @note in default close-on-exec mode is off. + * @return self + */ + inline TShellCommandOptions& SetCloseAllFdsOnExec(bool closeAllFdsOnExec) { + CloseAllFdsOnExec = closeAllFdsOnExec; + return *this; + } + + /** + * @brief set asynchronous mode. If true, task will be run + * in separate thread, and control will be returned immediately + * + * @param async true if asynchonous mode is needed + * @note in default async mode launcher will need 100% cpu for rapid process termination + * @return self + */ + inline TShellCommandOptions& SetAsync(bool async) { + AsyncMode = async; + if (AsyncMode) + PollDelayMs = 0; + return *this; + } + + /** + * @brief specify delay for process controlling loop + * @param ms number of milliseconds to poll for + * @note for synchronous process default of 1s should generally fit + * for async process default is no latency and that consumes 100% one cpu + * SetAsync(true) will reset this delay to 0, so call this method after + * @return self + */ + inline TShellCommandOptions& SetLatency(size_t ms) { + PollDelayMs = ms; + return *this; + } + + /** + * @brief set the stream, which is input fetched from + * + * @param stream Pointer to stream. + * If stream is NULL or not set, input channel will be closed. + * + * @return self + */ + inline TShellCommandOptions& SetInputStream(IInputStream* stream) { + InputStream = stream; + if (InputStream == nullptr) { + InputMode = HANDLE_INHERIT; + } else { + InputMode = HANDLE_STREAM; + } + return *this; + } + + /** + * @brief set the stream, collecting the command output + * + * @param stream Pointer to stream. + * If stream is NULL or not set, output will be collected to the + * internal variable + * + * @return self + */ + inline TShellCommandOptions& SetOutputStream(IOutputStream* stream) { + OutputStream = stream; + return *this; + } + + /** + * @brief set the stream, collecting the command error output + * + * @param stream Pointer to stream. + * If stream is NULL or not set, errors will be collected to the + * internal variable + * + * @return self + */ + inline TShellCommandOptions& SetErrorStream(IOutputStream* stream) { + ErrorStream = stream; + return *this; + } + + /** + * @brief set if Finish() should be called on user-supplied streams + * if process is run in async mode Finish will be called in process' thread + * @param val if Finish() should be called + * @return self + */ + inline TShellCommandOptions& SetCloseStreams(bool val) { + CloseStreams = val; + return *this; + } + + /** + * @brief set if input stream should be closed after all data is read + * call SetCloseInput(false) for interactive process + * @param val if input stream should be closed + * @return self + */ + inline TShellCommandOptions& SetCloseInput(bool val) { + ShouldCloseInput = val; + return *this; + } + + /** + * @brief set if command should be interpreted by OS shell (/bin/sh or cmd.exe) + * shell is enabled by default + * call SetUseShell(false) for command to be sent to OS verbatim + * @note shell operators > < | && || will not work if this option is off + * @param useShell if command should be run in shell + * @return self + */ + inline TShellCommandOptions& SetUseShell(bool useShell) { + UseShell = useShell; + if (!useShell) + QuoteArguments = false; + return *this; + } + + /** + * @brief set if the arguments should be wrapped in quotes. + * Please, note that this option makes no difference between + * real arguments and shell syntax, so if you execute something + * like \b TShellCommand("sleep") << "3" << "&&" << "ls", your + * command will look like: + * sleep "3" "&&" "ls" + * which will never end successfully. + * By default, this option is turned on. + * + * @note arguments will only be quoted if shell is used + * @param quote if the arguments should be quoted + * + * @return self + */ + inline TShellCommandOptions& SetQuoteArguments(bool quote) { + QuoteArguments = quote; + return *this; + } + + /** + * @brief set to run command in new session + * @note set this option to off to deliver parent's signals to command as well + * @note currently ignored on windows + * @param detach if command should be run in new session + * @return self + */ + inline TShellCommandOptions& SetDetachSession(bool detach) { + DetachSession = detach; + return *this; + } + + /** + * @brief specifies pure function to be called in the child process after fork, before calling execve + * @note currently ignored on windows + * @param function function to be called after fork + * @return self + */ + inline TShellCommandOptions& SetFuncAfterFork(const std::function<void()>& function) { + FuncAfterFork = function; + return *this; + } + + /** + * @brief create a pipe for child input + * Write end of the pipe will be accessible via TShellCommand::GetInputHandle + * + * @return self + */ + inline TShellCommandOptions& PipeInput() { + InputMode = HANDLE_PIPE; + InputStream = nullptr; + return *this; + } + + inline TShellCommandOptions& PipeOutput() { + OutputMode = HANDLE_PIPE; + OutputStream = nullptr; + return *this; + } + + inline TShellCommandOptions& PipeError() { + ErrorMode = HANDLE_PIPE; + ErrorStream = nullptr; + return *this; + } + + /** + * @brief set if child should inherit output handle + * + * @param inherit if child should inherit output handle + * + * @return self + */ + inline TShellCommandOptions& SetInheritOutput(bool inherit) { + OutputMode = inherit ? HANDLE_INHERIT : HANDLE_STREAM; + return *this; + } + + /** + * @brief set if child should inherit stderr handle + * + * @param inherit if child should inherit error output handle + * + * @return self + */ + inline TShellCommandOptions& SetInheritError(bool inherit) { + ErrorMode = inherit ? HANDLE_INHERIT : HANDLE_STREAM; + return *this; + } + +public: + bool ClearSignalMask = false; + bool CloseAllFdsOnExec = false; + bool AsyncMode = false; + size_t PollDelayMs = 0; + bool UseShell = false; + bool QuoteArguments = false; + bool DetachSession = false; + bool CloseStreams = false; + bool ShouldCloseInput = false; + EHandleMode InputMode = HANDLE_STREAM; + EHandleMode OutputMode = HANDLE_STREAM; + EHandleMode ErrorMode = HANDLE_STREAM; + + /// @todo more options + // bool SearchPath // search exe name in $PATH + // bool UnicodeConsole + // bool EmulateConsole // provide isatty == true + /// @todo command's stdin should be exposet as IOutputStream to support dialogue + IInputStream* InputStream; + IOutputStream* OutputStream; + IOutputStream* ErrorStream; + TUserOptions User; + THashMap<TString, TString> Environment; + int Nice = 0; + + static const size_t DefaultSyncPollDelay = 1000; // ms + std::function<void()> FuncAfterFork = {}; +}; + +/** + * @brief Execute command in shell and provide its results + * @attention Not thread-safe + */ +class TShellCommand: public TNonCopyable { +private: + TShellCommand(); + +public: + enum ECommandStatus { + SHELL_NONE, + SHELL_RUNNING, + SHELL_FINISHED, + SHELL_INTERNAL_ERROR, + SHELL_ERROR + }; + +public: + /** + * @brief create the command with initial arguments list + * + * @param cmd binary name + * @param args arguments list + * @param options execution options + * @todo store entire options structure + */ + TShellCommand(const TStringBuf cmd, const TList<TString>& args, const TShellCommandOptions& options = TShellCommandOptions(), + const TString& workdir = TString()); + TShellCommand(const TStringBuf cmd, const TShellCommandOptions& options = TShellCommandOptions(), const TString& workdir = TString()); + ~TShellCommand(); + +public: + /** + * @brief append argument to the args list + * + * @param argument string argument + * + * @return self + */ + TShellCommand& operator<<(const TStringBuf argument); + + /** + * @brief return the collected output from the command. + * If the output stream is set, empty string will be returned + * + * @return collected output + */ + const TString& GetOutput() const; + + /** + * @brief return the collected error output from the command. + * If the error stream is set, empty string will be returned + * + * @return collected error output + */ + const TString& GetError() const; + + /** + * @brief return the internal error occured while watching + * the command execution. Should be called if execution + * status is SHELL_INTERNAL_ERROR + * + * @return error text + */ + const TString& GetInternalError() const; + + /** + * @brief get current status of command execution + * + * @return current status + */ + ECommandStatus GetStatus() const; + + /** + * @brief return exit code of finished process + * The value is unspecified in case of internal errors or if the process is running + * + * @return exit code + */ + TMaybe<int> GetExitCode() const; + + /** + * @brief get id of underlying process + * @note depends on os: pid_t on UNIX, HANDLE on win + * + * @return pid or handle + */ + TProcessId GetPid() const; + + /** + * @brief return the file handle that provides input to the child process + * + * @return input file handle + */ + TFileHandle& GetInputHandle(); + + /** + * @brief return the file handle that provides output from the child process + * + * @return output file handle + */ + TFileHandle& GetOutputHandle(); + + /** + * @brief return the file handle that provides error output from the child process + * + * @return error file handle + */ + TFileHandle& GetErrorHandle(); + + /** + * @brief run the execution + * + * @return self + */ + TShellCommand& Run(); + + /** + * @brief terminate the execution + * @note if DetachSession is set, it terminates all procs in command's new process group + * + * @return self + */ + TShellCommand& Terminate(); + + /** + * @brief wait until the execution is finished + * + * @return self + */ + TShellCommand& Wait(); + + /** + * @brief close process' stdin + * + * @return self + */ + TShellCommand& CloseInput(); + + /** + * @brief Get quoted command (for debug/view purposes only!) + **/ + TString GetQuotedCommand() const; + +private: + class TImpl; + using TImplRef = TSimpleIntrusivePtr<TImpl>; + TImplRef Impl; +}; + +/// Appends to dst: quoted arg +void ShellQuoteArg(TString& dst, TStringBuf arg); + +/// Appends to dst: space, quoted arg +void ShellQuoteArgSp(TString& dst, TStringBuf arg); + +/// Returns true if arg should be quoted +bool ArgNeedsQuotes(TStringBuf arg) noexcept; |