diff --git a/CMakeLists.txt b/CMakeLists.txt index deb1bf2..5fdf639 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,16 +88,25 @@ add_executable(${PROJECT_NAME} src/config.cpp src/config.hpp src/utils.cpp src/utils.hpp src/widgets.cpp src/widgets.hpp + src/follow_process_log.hpp src/follow_process_log.cpp src/tui.cpp src/tui.hpp src/main.cpp ) add_executable(test-exec-interactive + src/config.cpp src/config.hpp + src/utils.cpp src/utils.hpp + src/tui.cpp src/tui.hpp + src/main_test.cpp + ) + +add_executable(test-process-tailing src/config.cpp src/utils.cpp src/widgets.cpp src/widgets.hpp + src/follow_process_log.hpp src/follow_process_log.cpp src/tui.cpp src/tui.hpp - src/main_test.cpp + src/test_proccess_tailing.cpp ) # Link this 'library' to use the warnings specified in CompilerWarnings.cmake @@ -114,6 +123,7 @@ include_directories(${CMAKE_SOURCE_DIR}/src) target_link_libraries(${PROJECT_NAME} PRIVATE project_warnings project_options spdlog::spdlog fmt::fmt ftxui::screen ftxui::dom ftxui::component nlohmann_json::nlohmann_json cpr::cpr PkgConfig::GLIBMM) target_link_libraries(test-exec-interactive PRIVATE project_warnings project_options spdlog::spdlog fmt::fmt ftxui::component cpr::cpr PkgConfig::GLIBMM) +target_link_libraries(test-process-tailing PRIVATE project_warnings project_options spdlog::spdlog fmt::fmt ftxui::component cpr::cpr PkgConfig::GLIBMM) if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") target_link_libraries(${PROJECT_NAME} PRIVATE range-v3::range-v3) diff --git a/meson.build b/meson.build index 9a84343..e55fbd0 100644 --- a/meson.build +++ b/meson.build @@ -47,6 +47,7 @@ src_files = files( 'src/config.cpp', 'src/config.hpp', 'src/utils.cpp', 'src/utils.hpp', 'src/widgets.cpp', 'src/widgets.hpp', + 'src/follow_process_log.cpp', 'src/follow_process_log.hpp', 'src/tui.cpp', 'src/tui.hpp', 'src/main.cpp', ) @@ -115,7 +116,7 @@ executable( executable( 'test-process-tailing', - files('src/config.cpp', 'src/widgets.cpp', 'src/tui.cpp', 'src/utils.cpp', 'src/test_proccess_tailing.cpp'), + files('src/config.cpp', 'src/widgets.cpp', 'src/tui.cpp', 'src/utils.cpp', 'src/follow_process_log.cpp','src/test_proccess_tailing.cpp'), dependencies: [fmt, spdlog, ftxui, cpr, glibmm], include_directories: [include_directories('src')], install: false) diff --git a/src/follow_process_log.cpp b/src/follow_process_log.cpp new file mode 100644 index 0000000..3e2d5d6 --- /dev/null +++ b/src/follow_process_log.cpp @@ -0,0 +1,69 @@ +#include "follow_process_log.hpp" +#include "subprocess.h" +#include "utils.hpp" // for make_multiline +#include "widgets.hpp" + +#include // for operator""s, chrono_literals +#include +#include // for string, operator<<, to_string +#include + +#include // for Renderer, Button +#include // for ButtonOption, Inpu... +#include +#include // for ScreenInteractive +#include // for operator|, text, Element, hbox, bold, color, filler, separator, vbox, window, gauge, Fit, size, dim, EQUAL, WIDTH + +using namespace ftxui; + +namespace tui::detail { + +void follow_process_log_widget(const std::vector& vec, Decorator boxsize) noexcept { + using namespace std::chrono_literals; + + bool running{true}; + std::string process_log{}; + subprocess_s child{}; + auto execute_thread = [&]() { + while (running) { + utils::exec_follow(vec, process_log, running, child); + std::this_thread::sleep_for(0.05s); + } + }; + std::jthread t(execute_thread); + auto screen = ScreenInteractive::Fullscreen(); + + std::jthread refresh_ui([&] { + while (running) { + std::this_thread::sleep_for(0.05s); + screen.PostEvent(Event::Custom); + } + }); + + auto handle_exit = [&]() { + if (child.child != 0) { + subprocess_terminate(&child); + subprocess_destroy(&child); + } + if (running) { + running = false; + refresh_ui.join(); + t.join(); + } + screen.ExitLoopClosure()(); + }; + + auto button_option = ButtonOption(); + button_option.border = false; + auto button_back = Button("Back", handle_exit, &button_option); + auto container = Container::Horizontal({button_back}); + + auto renderer = Renderer(container, [&] { + return tui::detail::centered_widget(container, "New CLI Installer", tui::detail::multiline_text(utils::make_multiline(process_log)) | boxsize | vscroll_indicator | yframe | flex); + }); + + screen.Loop(renderer); + running = false; +} + +} // namespace tui::detail diff --git a/src/follow_process_log.hpp b/src/follow_process_log.hpp new file mode 100644 index 0000000..0576c2d --- /dev/null +++ b/src/follow_process_log.hpp @@ -0,0 +1,14 @@ +#ifndef FOLLOW_PROCESS_LOG_HPP +#define FOLLOW_PROCESS_LOG_HPP + +#include // for string_view + +#include // for size, GREATER_THAN + +namespace tui { +namespace detail { + void follow_process_log_widget(const std::vector& vec, ftxui::Decorator boxsize = size(ftxui::HEIGHT, ftxui::GREATER_THAN, 5)) noexcept; +} // namespace detail +} // namespace tui + +#endif // FOLLOW_PROCESS_LOG_HPP diff --git a/src/subprocess.h b/src/subprocess.h new file mode 100644 index 0000000..3ad062f --- /dev/null +++ b/src/subprocess.h @@ -0,0 +1,1076 @@ +/* + The latest version of this library is available on GitHub; + https://github.com/sheredom/subprocess.h +*/ + +/* + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + For more information, please refer to +*/ + +#ifndef SHEREDOM_SUBPROCESS_H_INCLUDED +#define SHEREDOM_SUBPROCESS_H_INCLUDED + +#if defined(_MSC_VER) +#pragma warning(push, 1) +#endif + +#include + +#if defined(_MSC_VER) +#pragma warning(pop) +#endif + +#if defined(_MSC_VER) +#define subprocess_pure +#define subprocess_weak __inline +#elif defined(__clang__) || defined(__GNUC__) +#define subprocess_pure __attribute__((pure)) +#define subprocess_weak __attribute__((weak)) +#else +#error Non clang, non gcc, non MSVC compiler found! +#endif + +struct subprocess_s; + +enum subprocess_option_e { + // stdout and stderr are the same FILE. + subprocess_option_combined_stdout_stderr = 0x1, + + // The child process should inherit the environment variables of the parent. + subprocess_option_inherit_environment = 0x2, + + // Enable asynchronous reading of stdout/stderr before it has completed. + subprocess_option_enable_async = 0x4, + + // Enable the child process to be spawned with no window visible if supported + // by the platform. + subprocess_option_no_window = 0x8 +}; + +#if defined(__cplusplus) +extern "C" { +#endif + +/// @brief Create a process. +/// @param command_line An array of strings for the command line to execute for +/// this process. The last element must be NULL to signify the end of the array. +/// The memory backing this parameter only needs to persist until this function +/// returns. +/// @param options A bit field of subprocess_option_e's to pass. +/// @param out_process The newly created process. +/// @return On success zero is returned. +subprocess_weak int subprocess_create(const char *const command_line[], + int options, + struct subprocess_s *const out_process); + +/// @brief Create a process (extended create). +/// @param command_line An array of strings for the command line to execute for +/// this process. The last element must be NULL to signify the end of the array. +/// The memory backing this parameter only needs to persist until this function +/// returns. +/// @param options A bit field of subprocess_option_e's to pass. +/// @param environment An optional array of strings for the environment to use +/// for a child process (each element of the form FOO=BAR). The last element +/// must be NULL to signify the end of the array. +/// @param out_process The newly created process. +/// @return On success zero is returned. +/// +/// If `options` contains `subprocess_option_inherit_environment`, then +/// `environment` must be NULL. +subprocess_weak int +subprocess_create_ex(const char *const command_line[], int options, + const char *const environment[], + struct subprocess_s *const out_process); + +/// @brief Get the standard input file for a process. +/// @param process The process to query. +/// @return The file for standard input of the process. +/// +/// The file returned can be written to by the parent process to feed data to +/// the standard input of the process. +subprocess_pure subprocess_weak FILE * +subprocess_stdin(const struct subprocess_s *const process); + +/// @brief Get the standard output file for a process. +/// @param process The process to query. +/// @return The file for standard output of the process. +/// +/// The file returned can be read from by the parent process to read data from +/// the standard output of the child process. +subprocess_pure subprocess_weak FILE * +subprocess_stdout(const struct subprocess_s *const process); + +/// @brief Get the standard error file for a process. +/// @param process The process to query. +/// @return The file for standard error of the process. +/// +/// The file returned can be read from by the parent process to read data from +/// the standard error of the child process. +/// +/// If the process was created with the subprocess_option_combined_stdout_stderr +/// option bit set, this function will return NULL, and the subprocess_stdout +/// function should be used for both the standard output and error combined. +subprocess_pure subprocess_weak FILE * +subprocess_stderr(const struct subprocess_s *const process); + +/// @brief Wait for a process to finish execution. +/// @param process The process to wait for. +/// @param out_return_code The return code of the returned process (can be +/// NULL). +/// @return On success zero is returned. +/// +/// Joining a process will close the stdin pipe to the process. +subprocess_weak int subprocess_join(struct subprocess_s *const process, + int *const out_return_code); + +/// @brief Destroy a previously created process. +/// @param process The process to destroy. +/// @return On success zero is returned. +/// +/// If the process to be destroyed had not finished execution, it may out live +/// the parent process. +subprocess_weak int subprocess_destroy(struct subprocess_s *const process); + +/// @brief Terminate a previously created process. +/// @param process The process to terminate. +/// @return On success zero is returned. +/// +/// If the process to be destroyed had not finished execution, it will be +/// terminated (i.e killed). +subprocess_weak int subprocess_terminate(struct subprocess_s *const process); + +/// @brief Read the standard output from the child process. +/// @param process The process to read from. +/// @param buffer The buffer to read into. +/// @param size The maximum number of bytes to read. +/// @return The number of bytes actually read into buffer. Can only be 0 if the +/// process has complete. +/// +/// The only safe way to read from the standard output of a process during it's +/// execution is to use the `subprocess_option_enable_async` option in +/// conjuction with this method. +subprocess_weak unsigned +subprocess_read_stdout(struct subprocess_s *const process, char *const buffer, + unsigned size); + +/// @brief Read the standard error from the child process. +/// @param process The process to read from. +/// @param buffer The buffer to read into. +/// @param size The maximum number of bytes to read. +/// @return The number of bytes actually read into buffer. Can only be 0 if the +/// process has complete. +/// +/// The only safe way to read from the standard error of a process during it's +/// execution is to use the `subprocess_option_enable_async` option in +/// conjuction with this method. +subprocess_weak unsigned +subprocess_read_stderr(struct subprocess_s *const process, char *const buffer, + unsigned size); + +/// @brief Returns if the subprocess is currently still alive and executing. +/// @param process The process to check. +/// @return If the process is still alive non-zero is returned. +subprocess_weak int subprocess_alive(struct subprocess_s *const process); + +#if defined(__cplusplus) +#define SUBPROCESS_CAST(type, x) static_cast(x) +#define SUBPROCESS_PTR_CAST(type, x) reinterpret_cast(x) +#define SUBPROCESS_CONST_CAST(type, x) const_cast(x) +#define SUBPROCESS_NULL NULL +#else +#define SUBPROCESS_CAST(type, x) ((type)(x)) +#define SUBPROCESS_PTR_CAST(type, x) ((type)(x)) +#define SUBPROCESS_CONST_CAST(type, x) ((type)(x)) +#define SUBPROCESS_NULL 0 +#endif + +#if !defined(_MSC_VER) +#include +#include +#include +#include +#include +#endif + +#if defined(_MSC_VER) + +#if (_MSC_VER < 1920) +#ifdef _WIN64 +typedef __int64 subprocess_intptr_t; +typedef unsigned __int64 subprocess_size_t; +#else +typedef int subprocess_intptr_t; +typedef unsigned int subprocess_size_t; +#endif +#else +#include + +typedef intptr_t subprocess_intptr_t; +typedef size_t subprocess_size_t; +#endif + +typedef struct _PROCESS_INFORMATION *LPPROCESS_INFORMATION; +typedef struct _SECURITY_ATTRIBUTES *LPSECURITY_ATTRIBUTES; +typedef struct _STARTUPINFOA *LPSTARTUPINFOA; +typedef struct _OVERLAPPED *LPOVERLAPPED; + +#pragma warning(push, 1) +struct subprocess_subprocess_information_s { + void *hProcess; + void *hThread; + unsigned long dwProcessId; + unsigned long dwThreadId; +}; + +struct subprocess_security_attributes_s { + unsigned long nLength; + void *lpSecurityDescriptor; + int bInheritHandle; +}; + +struct subprocess_startup_info_s { + unsigned long cb; + char *lpReserved; + char *lpDesktop; + char *lpTitle; + unsigned long dwX; + unsigned long dwY; + unsigned long dwXSize; + unsigned long dwYSize; + unsigned long dwXCountChars; + unsigned long dwYCountChars; + unsigned long dwFillAttribute; + unsigned long dwFlags; + unsigned short wShowWindow; + unsigned short cbReserved2; + unsigned char *lpReserved2; + void *hStdInput; + void *hStdOutput; + void *hStdError; +}; + +struct subprocess_overlapped_s { + uintptr_t Internal; + uintptr_t InternalHigh; + union { + struct { + unsigned long Offset; + unsigned long OffsetHigh; + } DUMMYSTRUCTNAME; + void *Pointer; + } DUMMYUNIONNAME; + + void *hEvent; +}; + +#pragma warning(pop) + +__declspec(dllimport) unsigned long __stdcall GetLastError(void); +__declspec(dllimport) int __stdcall SetHandleInformation(void *, unsigned long, + unsigned long); +__declspec(dllimport) int __stdcall CreatePipe(void **, void **, + LPSECURITY_ATTRIBUTES, + unsigned long); +__declspec(dllimport) void *__stdcall CreateNamedPipeA( + const char *, unsigned long, unsigned long, unsigned long, unsigned long, + unsigned long, unsigned long, LPSECURITY_ATTRIBUTES); +__declspec(dllimport) int __stdcall ReadFile(void *, void *, unsigned long, + unsigned long *, LPOVERLAPPED); +__declspec(dllimport) unsigned long __stdcall GetCurrentProcessId(void); +__declspec(dllimport) unsigned long __stdcall GetCurrentThreadId(void); +__declspec(dllimport) void *__stdcall CreateFileA(const char *, unsigned long, + unsigned long, + LPSECURITY_ATTRIBUTES, + unsigned long, unsigned long, + void *); +__declspec(dllimport) void *__stdcall CreateEventA(LPSECURITY_ATTRIBUTES, int, + int, const char *); +__declspec(dllimport) int __stdcall CreateProcessA( + const char *, char *, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, int, + unsigned long, void *, const char *, LPSTARTUPINFOA, LPPROCESS_INFORMATION); +__declspec(dllimport) int __stdcall CloseHandle(void *); +__declspec(dllimport) unsigned long __stdcall WaitForSingleObject( + void *, unsigned long); +__declspec(dllimport) int __stdcall GetExitCodeProcess( + void *, unsigned long *lpExitCode); +__declspec(dllimport) int __stdcall TerminateProcess(void *, unsigned int); +__declspec(dllimport) unsigned long __stdcall WaitForMultipleObjects( + unsigned long, void *const *, int, unsigned long); +__declspec(dllimport) int __stdcall GetOverlappedResult(void *, LPOVERLAPPED, + unsigned long *, int); + +#if defined(_DLL) && (_DLL == 1) +#define SUBPROCESS_DLLIMPORT __declspec(dllimport) +#else +#define SUBPROCESS_DLLIMPORT +#endif + +SUBPROCESS_DLLIMPORT int __cdecl _fileno(FILE *); +SUBPROCESS_DLLIMPORT int __cdecl _open_osfhandle(subprocess_intptr_t, int); +SUBPROCESS_DLLIMPORT subprocess_intptr_t __cdecl _get_osfhandle(int); + +void *__cdecl _alloca(subprocess_size_t); +#else +typedef size_t subprocess_size_t; +#endif + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpadded" +#endif +struct subprocess_s { + FILE *stdin_file; + FILE *stdout_file; + FILE *stderr_file; + +#if defined(_MSC_VER) + void *hProcess; + void *hStdInput; + void *hEventOutput; + void *hEventError; +#else + pid_t child; + int return_status; +#endif + + subprocess_size_t alive; +}; +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#if defined(_MSC_VER) +subprocess_weak int subprocess_create_named_pipe_helper(void **rd, void **wr); +int subprocess_create_named_pipe_helper(void **rd, void **wr) { + const unsigned long pipeAccessInbound = 0x00000001; + const unsigned long fileFlagOverlapped = 0x40000000; + const unsigned long pipeTypeByte = 0x00000000; + const unsigned long pipeWait = 0x00000000; + const unsigned long genericWrite = 0x40000000; + const unsigned long openExisting = 3; + const unsigned long fileAttributeNormal = 0x00000080; + const void *const invalidHandleValue = + SUBPROCESS_PTR_CAST(void *, ~(SUBPROCESS_CAST(subprocess_intptr_t, 0))); + struct subprocess_security_attributes_s saAttr = {sizeof(saAttr), + SUBPROCESS_NULL, 1}; + char name[256] = {0}; + __declspec(thread) static long index = 0; + const long unique = index++; + +#if _MSC_VER < 1900 +#pragma warning(disable : 4996) +#pragma warning(push, 1) + + _snprintf(name, sizeof(name) - 1, + "\\\\.\\pipe\\sheredom_subprocess_h.%08lx.%08lx.%ld", + GetCurrentProcessId(), GetCurrentThreadId(), unique); + +#pragma warning(pop) +#else +#pragma warning(disable : 4710) +#pragma warning(push, 1) + snprintf(name, sizeof(name) - 1, + "\\\\.\\pipe\\sheredom_subprocess_h.%08lx.%08lx.%ld", + GetCurrentProcessId(), GetCurrentThreadId(), unique); +#pragma warning(pop) +#endif + + *rd = + CreateNamedPipeA(name, pipeAccessInbound | fileFlagOverlapped, + pipeTypeByte | pipeWait, 1, 4096, 4096, SUBPROCESS_NULL, + SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr)); + + if (invalidHandleValue == rd) { + return -1; + } + + *wr = CreateFileA(name, genericWrite, SUBPROCESS_NULL, + SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), + openExisting, fileAttributeNormal, SUBPROCESS_NULL); + + if (invalidHandleValue == wr) { + return -1; + } + + return 0; +} +#endif + +int subprocess_create(const char *const commandLine[], int options, + struct subprocess_s *const out_process) { + return subprocess_create_ex(commandLine, options, SUBPROCESS_NULL, + out_process); +} + +int subprocess_create_ex(const char *const commandLine[], int options, + const char *const environment[], + struct subprocess_s *const out_process) { +#if defined(_MSC_VER) + int fd; + void *rd, *wr; + char *commandLineCombined; + subprocess_size_t len; + int i, j; + unsigned long flags = 0; + const unsigned long startFUseStdHandles = 0x00000100; + const unsigned long handleFlagInherit = 0x00000001; + const unsigned long createNoWindow = 0x08000000; + struct subprocess_subprocess_information_s processInfo; + struct subprocess_security_attributes_s saAttr = {sizeof(saAttr), + SUBPROCESS_NULL, 1}; + char *used_environment = SUBPROCESS_NULL; + struct subprocess_startup_info_s startInfo = {0, + SUBPROCESS_NULL, + SUBPROCESS_NULL, + SUBPROCESS_NULL, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + SUBPROCESS_NULL, + SUBPROCESS_NULL, + SUBPROCESS_NULL, + SUBPROCESS_NULL}; + + startInfo.cb = sizeof(startInfo); + startInfo.dwFlags = startFUseStdHandles; + + if (subprocess_option_no_window != (options & subprocess_option_no_window)) { + flags |= createNoWindow; + } + + if (subprocess_option_inherit_environment != + (options & subprocess_option_inherit_environment)) { + if (SUBPROCESS_NULL == environment) { + used_environment = SUBPROCESS_CONST_CAST(char *, "\0\0"); + } else { + // We always end with two null terminators. + len = 2; + + for (i = 0; environment[i]; i++) { + for (j = 0; '\0' != environment[i][j]; j++) { + len++; + } + + // For the null terminator too. + len++; + } + + used_environment = SUBPROCESS_CAST(char *, _alloca(len)); + + // Re-use len for the insertion position + len = 0; + + for (i = 0; environment[i]; i++) { + for (j = 0; '\0' != environment[i][j]; j++) { + used_environment[len++] = environment[i][j]; + } + + used_environment[len++] = '\0'; + } + + // End with the two null terminators. + used_environment[len++] = '\0'; + used_environment[len++] = '\0'; + } + } else { + if (SUBPROCESS_NULL != environment) { + return -1; + } + } + + if (!CreatePipe(&rd, &wr, SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), + 0)) { + return -1; + } + + if (!SetHandleInformation(wr, handleFlagInherit, 0)) { + return -1; + } + + fd = _open_osfhandle(SUBPROCESS_PTR_CAST(subprocess_intptr_t, wr), 0); + + if (-1 != fd) { + out_process->stdin_file = _fdopen(fd, "wb"); + + if (SUBPROCESS_NULL == out_process->stdin_file) { + return -1; + } + } + + startInfo.hStdInput = rd; + + if (options & subprocess_option_enable_async) { + if (subprocess_create_named_pipe_helper(&rd, &wr)) { + return -1; + } + } else { + if (!CreatePipe(&rd, &wr, + SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 0)) { + return -1; + } + } + + if (!SetHandleInformation(rd, handleFlagInherit, 0)) { + return -1; + } + + fd = _open_osfhandle(SUBPROCESS_PTR_CAST(subprocess_intptr_t, rd), 0); + + if (-1 != fd) { + out_process->stdout_file = _fdopen(fd, "rb"); + + if (SUBPROCESS_NULL == out_process->stdout_file) { + return -1; + } + } + + startInfo.hStdOutput = wr; + + if (subprocess_option_combined_stdout_stderr == + (options & subprocess_option_combined_stdout_stderr)) { + out_process->stderr_file = out_process->stdout_file; + startInfo.hStdError = startInfo.hStdOutput; + } else { + if (options & subprocess_option_enable_async) { + if (subprocess_create_named_pipe_helper(&rd, &wr)) { + return -1; + } + } else { + if (!CreatePipe(&rd, &wr, + SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 0)) { + return -1; + } + } + + if (!SetHandleInformation(rd, handleFlagInherit, 0)) { + return -1; + } + + fd = _open_osfhandle(SUBPROCESS_PTR_CAST(subprocess_intptr_t, rd), 0); + + if (-1 != fd) { + out_process->stderr_file = _fdopen(fd, "rb"); + + if (SUBPROCESS_NULL == out_process->stderr_file) { + return -1; + } + } + + startInfo.hStdError = wr; + } + + if (options & subprocess_option_enable_async) { + out_process->hEventOutput = + CreateEventA(SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 1, 1, + SUBPROCESS_NULL); + out_process->hEventError = + CreateEventA(SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 1, 1, + SUBPROCESS_NULL); + } else { + out_process->hEventOutput = SUBPROCESS_NULL; + out_process->hEventError = SUBPROCESS_NULL; + } + + // Combine commandLine together into a single string + len = 0; + for (i = 0; commandLine[i]; i++) { + // For the ' ' and two '"' between items and trailing '\0' + len += 3; + + for (j = 0; '\0' != commandLine[i][j]; j++) { + switch (commandLine[i][j]) { + default: + break; + case '\\': + if (commandLine[i][j + 1] == '"') { + len++; + } + + break; + case '"': + len++; + break; + } + len++; + } + } + + commandLineCombined = SUBPROCESS_CAST(char *, _alloca(len)); + + if (!commandLineCombined) { + return -1; + } + + // Gonna re-use len to store the write index into commandLineCombined + len = 0; + + for (i = 0; commandLine[i]; i++) { + if (0 != i) { + commandLineCombined[len++] = ' '; + } + commandLineCombined[len++] = '"'; + + for (j = 0; '\0' != commandLine[i][j]; j++) { + switch (commandLine[i][j]) { + default: + break; + case '\\': + if (commandLine[i][j + 1] == '"') { + commandLineCombined[len++] = '\\'; + } + + break; + case '"': + commandLineCombined[len++] = '\\'; + break; + } + + commandLineCombined[len++] = commandLine[i][j]; + } + commandLineCombined[len++] = '"'; + } + + commandLineCombined[len] = '\0'; + + if (!CreateProcessA( + SUBPROCESS_NULL, + commandLineCombined, // command line + SUBPROCESS_NULL, // process security attributes + SUBPROCESS_NULL, // primary thread security attributes + 1, // handles are inherited + createNoWindow, // creation flags + used_environment, // used environment + SUBPROCESS_NULL, // use parent's current directory + SUBPROCESS_PTR_CAST(LPSTARTUPINFOA, + &startInfo), // STARTUPINFO pointer + SUBPROCESS_PTR_CAST(LPPROCESS_INFORMATION, &processInfo))) { + return -1; + } + + out_process->hProcess = processInfo.hProcess; + + out_process->hStdInput = startInfo.hStdInput; + + // We don't need the handle of the primary thread in the called process. + CloseHandle(processInfo.hThread); + + if (SUBPROCESS_NULL != startInfo.hStdOutput) { + CloseHandle(startInfo.hStdOutput); + + if (startInfo.hStdError != startInfo.hStdOutput) { + CloseHandle(startInfo.hStdError); + } + } + + out_process->alive = 1; + + return 0; +#else + int stdinfd[2]; + int stdoutfd[2]; + int stderrfd[2]; + pid_t child; + + if (subprocess_option_inherit_environment == + (options & subprocess_option_inherit_environment)) { + if (SUBPROCESS_NULL != environment) { + return -1; + } + } + + if (0 != pipe(stdinfd)) { + return -1; + } + + if (0 != pipe(stdoutfd)) { + return -1; + } + + if (subprocess_option_combined_stdout_stderr != + (options & subprocess_option_combined_stdout_stderr)) { + if (0 != pipe(stderrfd)) { + return -1; + } + } + + child = fork(); + + if (-1 == child) { + return -1; + } + + if (0 == child) { + // Close the stdin write end + close(stdinfd[1]); + // Map the read end to stdin + dup2(stdinfd[0], STDIN_FILENO); + + // Close the stdout read end + close(stdoutfd[0]); + // Map the write end to stdout + dup2(stdoutfd[1], STDOUT_FILENO); + + if (subprocess_option_combined_stdout_stderr == + (options & subprocess_option_combined_stdout_stderr)) { + dup2(STDOUT_FILENO, STDERR_FILENO); + } else { + // Close the stderr read end + close(stderrfd[0]); + // Map the write end to stdout + dup2(stderrfd[1], STDERR_FILENO); + } + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-qual" +#pragma clang diagnostic ignored "-Wold-style-cast" +#endif + + if (environment) { + exit(execve(commandLine[0], (char *const *)commandLine, + (char *const *)environment)); + } else if (subprocess_option_inherit_environment != + (options & subprocess_option_inherit_environment)) { + char *const empty_environment[1] = {SUBPROCESS_NULL}; + exit(execve(commandLine[0], (char *const *)commandLine, + empty_environment)); + } else { + exit(execv(commandLine[0], (char *const *)commandLine)); + } + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + } else { + // Close the stdin read end + close(stdinfd[0]); + // Store the stdin write end + out_process->stdin_file = fdopen(stdinfd[1], "wb"); + + // Close the stdout write end + close(stdoutfd[1]); + // Store the stdout read end + out_process->stdout_file = fdopen(stdoutfd[0], "rb"); + + if (subprocess_option_combined_stdout_stderr == + (options & subprocess_option_combined_stdout_stderr)) { + out_process->stderr_file = out_process->stdout_file; + } else { + // Close the stderr write end + close(stderrfd[1]); + // Store the stderr read end + out_process->stderr_file = fdopen(stderrfd[0], "rb"); + } + + // Store the child's pid + out_process->child = child; + + out_process->alive = 1; + + return 0; + } +#endif +} + +FILE *subprocess_stdin(const struct subprocess_s *const process) { + return process->stdin_file; +} + +FILE *subprocess_stdout(const struct subprocess_s *const process) { + return process->stdout_file; +} + +FILE *subprocess_stderr(const struct subprocess_s *const process) { + if (process->stdout_file != process->stderr_file) { + return process->stderr_file; + } else { + return SUBPROCESS_NULL; + } +} + +int subprocess_join(struct subprocess_s *const process, + int *const out_return_code) { +#if defined(_MSC_VER) + const unsigned long infinite = 0xFFFFFFFF; + + if (process->stdin_file) { + fclose(process->stdin_file); + process->stdin_file = SUBPROCESS_NULL; + } + + if (process->hStdInput) { + CloseHandle(process->hStdInput); + process->hStdInput = SUBPROCESS_NULL; + } + + WaitForSingleObject(process->hProcess, infinite); + + if (out_return_code) { + if (!GetExitCodeProcess( + process->hProcess, + SUBPROCESS_PTR_CAST(unsigned long *, out_return_code))) { + return -1; + } + } + + process->alive = 0; + + return 0; +#else + int status; + + if (process->stdin_file) { + fclose(process->stdin_file); + process->stdin_file = SUBPROCESS_NULL; + } + + if (process->child) { + if (process->child != waitpid(process->child, &status, 0)) { + return -1; + } + + process->child = 0; + + if (WIFEXITED(status)) { + process->return_status = WEXITSTATUS(status); + } else { + process->return_status = EXIT_FAILURE; + } + + process->alive = 0; + } + + if (out_return_code) { + *out_return_code = process->return_status; + } + + return 0; +#endif +} + +int subprocess_destroy(struct subprocess_s *const process) { + if (process->stdin_file) { + fclose(process->stdin_file); + process->stdin_file = SUBPROCESS_NULL; + } + + if (process->stdout_file) { + fclose(process->stdout_file); + + if (process->stdout_file != process->stderr_file) { + fclose(process->stderr_file); + } + + process->stdout_file = SUBPROCESS_NULL; + process->stderr_file = SUBPROCESS_NULL; + } + +#if defined(_MSC_VER) + if (process->hProcess) { + CloseHandle(process->hProcess); + process->hProcess = SUBPROCESS_NULL; + + if (process->hStdInput) { + CloseHandle(process->hStdInput); + } + + if (process->hEventOutput) { + CloseHandle(process->hEventOutput); + } + + if (process->hEventError) { + CloseHandle(process->hEventError); + } + } +#endif + + return 0; +} + +int subprocess_terminate(struct subprocess_s *const process) { +#if defined(_MSC_VER) + unsigned int killed_process_exit_code; + int success_terminate; + int windows_call_result; + + killed_process_exit_code = 99; + windows_call_result = + TerminateProcess(process->hProcess, killed_process_exit_code); + success_terminate = (windows_call_result == 0) ? 1 : 0; + return success_terminate; +#else + int result; + result = kill(process->child, 9); + return result; +#endif +} + +unsigned subprocess_read_stdout(struct subprocess_s *const process, + char *const buffer, unsigned size) { +#if defined(_MSC_VER) + void *handle; + unsigned long bytes_read = 0; + struct subprocess_overlapped_s overlapped = {0, 0, {{0, 0}}, SUBPROCESS_NULL}; + overlapped.hEvent = process->hEventOutput; + + handle = SUBPROCESS_PTR_CAST(void *, + _get_osfhandle(_fileno(process->stdout_file))); + + if (!ReadFile(handle, buffer, size, &bytes_read, + SUBPROCESS_PTR_CAST(LPOVERLAPPED, &overlapped))) { + const unsigned long errorIoPending = 997; + unsigned long error = GetLastError(); + + // Means we've got an async read! + if (error == errorIoPending) { + if (!GetOverlappedResult(handle, + SUBPROCESS_PTR_CAST(LPOVERLAPPED, &overlapped), + &bytes_read, 1)) { + const unsigned long errorIoIncomplete = 996; + const unsigned long errorHandleEOF = 38; + error = GetLastError(); + + if ((error != errorIoIncomplete) && (error != errorHandleEOF)) { + return 0; + } + } + } + } + + return SUBPROCESS_CAST(unsigned, bytes_read); +#else + const int fd = fileno(process->stdout_file); + const ssize_t bytes_read = read(fd, buffer, size); + + if (bytes_read < 0) { + return 0; + } + + return SUBPROCESS_CAST(unsigned, bytes_read); +#endif +} + +unsigned subprocess_read_stderr(struct subprocess_s *const process, + char *const buffer, unsigned size) { +#if defined(_MSC_VER) + void *handle; + unsigned long bytes_read = 0; + struct subprocess_overlapped_s overlapped = {0, 0, {{0, 0}}, SUBPROCESS_NULL}; + overlapped.hEvent = process->hEventError; + + handle = SUBPROCESS_PTR_CAST(void *, + _get_osfhandle(_fileno(process->stderr_file))); + + if (!ReadFile(handle, buffer, size, &bytes_read, + SUBPROCESS_PTR_CAST(LPOVERLAPPED, &overlapped))) { + const unsigned long errorIoPending = 997; + unsigned long error = GetLastError(); + + // Means we've got an async read! + if (error == errorIoPending) { + if (!GetOverlappedResult(handle, + SUBPROCESS_PTR_CAST(LPOVERLAPPED, &overlapped), + &bytes_read, 1)) { + const unsigned long errorIoIncomplete = 996; + const unsigned long errorHandleEOF = 38; + error = GetLastError(); + + if ((error != errorIoIncomplete) && (error != errorHandleEOF)) { + return 0; + } + } + } + } + + return SUBPROCESS_CAST(unsigned, bytes_read); +#else + const int fd = fileno(process->stderr_file); + const ssize_t bytes_read = read(fd, buffer, size); + + if (bytes_read < 0) { + return 0; + } + + return SUBPROCESS_CAST(unsigned, bytes_read); +#endif +} + +int subprocess_alive(struct subprocess_s *const process) { + int is_alive = SUBPROCESS_CAST(int, process->alive); + + if (!is_alive) { + return 0; + } +#if defined(_MSC_VER) + { + const unsigned long zero = 0x0; + const unsigned long wait_object_0 = 0x00000000L; + + is_alive = wait_object_0 != WaitForSingleObject(process->hProcess, zero); + } +#else + { + int status; + is_alive = 0 == waitpid(process->child, &status, WNOHANG); + + // If the process was successfully waited on we need to cleanup now. + if (!is_alive) { + if (WIFEXITED(status)) { + process->return_status = WEXITSTATUS(status); + } else { + process->return_status = EXIT_FAILURE; + } + + // Since we've already successfully waited on the process, we need to wipe + // the child now. + process->child = 0; + + if (subprocess_join(process, SUBPROCESS_NULL)) { + return -1; + } + } + } +#endif + + if (!is_alive) { + process->alive = 0; + } + + return is_alive; +} + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif /* SHEREDOM_SUBPROCESS_H_INCLUDED */ diff --git a/src/test_proccess_tailing.cpp b/src/test_proccess_tailing.cpp index bb14587..12fcb64 100644 --- a/src/test_proccess_tailing.cpp +++ b/src/test_proccess_tailing.cpp @@ -1,95 +1,5 @@ -#include -#include - -#include // for operator""s, chrono_literals -#include // for Renderer, Button -#include // for ButtonOption, Inpu... -#include // for ScreenInteractive -#include // for operator|, text, Element, hbox, bold, color, filler, separator, vbox, window, gauge, Fit, size, dim, EQUAL, WIDTH -#include // for Full, Screen -#include // for cout, endl, ostream -#include // for list, operator!=, _List_iterator, _List_iterator<>::_Self -#include // for allocator, shared_ptr, allocator_traits<>::value_type -#include // for string, operator<<, to_string -#include // for sleep_for -#include // for move -#include // for vector - -#include -#include -#include // for Render -#include // for ftxui -#include // for Color, Color::Green, Color::Red, Color::RedLight - -#include "subprocess.h" -#include "utils.hpp" -#include "widgets.hpp" - -static bool running{true}; -static std::string process_log{}; - -using namespace std::chrono_literals; - -void execute() { - const char* const commandLine[] = {"/sbin/tail", "-f", "/tmp/smth.log", nullptr}; - struct subprocess_s process; - int ret = -1; - static char data[1048576 + 1] = {0}; - unsigned index = 0; - unsigned bytes_read = 0; - - if ((ret = subprocess_create(commandLine, subprocess_option_enable_async | subprocess_option_combined_stdout_stderr, &process)) != 0) { - std::perror("creation failed"); - std::cerr << "creation failed with: " << ret << '\n'; - running = false; - return; - } - - do { - bytes_read = subprocess_read_stdout(&process, data + index, - sizeof(data) - 1 - index); - - index += bytes_read; - - process_log = data; - } while (bytes_read != 0); - - subprocess_join(&process, &ret); - subprocess_destroy(&process); -} - -void execute_thread() { - while (running) { - execute(); - - std::this_thread::sleep_for(2s); - } -} +#include "follow_process_log.hpp" int main() { - using namespace ftxui; - std::jthread t(execute_thread); - - auto screen = ScreenInteractive::Fullscreen(); - - std::jthread refresh_ui([&] { - while (running) { - std::this_thread::sleep_for(0.05s); - screen.PostEvent(Event::Custom); - } - }); - - auto button_option = ButtonOption(); - button_option.border = false; - auto button_back = Button("Back", screen.ExitLoopClosure(), &button_option); - auto container = Container::Horizontal({button_back}); - - auto renderer = Renderer(container, [&] { - return tui::detail::centered_widget(container, "New CLI Installer", tui::detail::multiline_text(utils::make_multiline(process_log)) | vscroll_indicator | yframe | flex); - }); - - screen.Loop(renderer); - running = false; - t.join(); - refresh_ui.join(); + tui::detail::follow_process_log_widget({"/sbin/tail", "-f", "/tmp/smth.log"}); } diff --git a/src/tui.cpp b/src/tui.cpp index 4e2a3f0..e142ce8 100644 --- a/src/tui.cpp +++ b/src/tui.cpp @@ -30,6 +30,10 @@ namespace fs = std::filesystem; namespace ranges = std::ranges; #endif +#ifdef NDEVENV +#include "follow_process_log.hpp" +#endif + namespace tui { // Revised to deal with partion sizes now being displayed to the user @@ -418,10 +422,12 @@ void install_cust_pkgs() noexcept { const auto& hostcache = std::get(config_data["hostcache"]); if (hostcache) { - utils::exec(fmt::format("pacstrap {} {}", mountpoint, packages)); + // utils::exec(fmt::format("pacstrap {} {}", mountpoint, packages)); + detail::follow_process_log_widget({"/sbin/pacstrap", mountpoint, packages}); return; } - utils::exec(fmt::format("pacstrap -c {} {}", mountpoint, packages)); + // utils::exec(fmt::format("pacstrap -c {} {}", mountpoint, packages)); + detail::follow_process_log_widget({"/sbin/pacstrap", "-c", mountpoint, packages}); #endif } @@ -454,7 +460,8 @@ void install_systemd_boot() noexcept { const auto& uefi_mount = std::get(config_data["UEFI_MOUNT"]); utils::arch_chroot(fmt::format("bootctl --path={} install", uefi_mount)); - utils::exec(fmt::format("pacstrap {} systemd-boot-manager", mountpoint)); + // utils::exec(fmt::format("pacstrap {} systemd-boot-manager", mountpoint)); + detail::follow_process_log_widget({"/sbin/pacstrap", mountpoint, "systemd-boot-manager"}); utils::arch_chroot("sdboot-manage gen"); // Check if the volume is removable. If so, dont use autodetect @@ -603,7 +610,8 @@ void install_base() noexcept { spdlog::info(fmt::format("Preparing for pkgs to install: \"{}\"", packages)); #ifdef NDEVENV // filter_packages - utils::exec(fmt::format("pacstrap {} {} |& tee /tmp/pacstrap.log", mountpoint, packages)); + // utils::exec(fmt::format("pacstrap {} {} |& tee /tmp/pacstrap.log", mountpoint, packages)); + detail::follow_process_log_widget({"/sbin/pacstrap", mountpoint, packages}); #endif std::ofstream{base_installed}; } diff --git a/src/utils.cpp b/src/utils.cpp index c404990..5dc3f0d 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1,6 +1,7 @@ #include "utils.hpp" #include "config.hpp" #include "definitions.hpp" +#include "subprocess.h" #include "tui.hpp" #include "widgets.hpp" @@ -82,6 +83,43 @@ void arch_chroot(const std::string_view& command) noexcept { utils::exec(fmt::format("arch-chroot {} \"{}\"", mountpoint, command)); } +void exec_follow(const std::vector& vec, std::string& process_log, bool& running, subprocess_s& child, bool async) noexcept { + if (!async) { + spdlog::error("Implement me!"); + return; + } + subprocess_s process{}; + int32_t ret{-1}; + static std::array data{}; + uint32_t index{}; + uint32_t bytes_read{}; + + std::vector args; + std::transform(vec.cbegin(), vec.cend(), std::back_inserter(args), + [=](const std::string& arg) -> char* { return const_cast(arg.data()); }); + args.push_back(nullptr); + + char** command = args.data(); + + if ((ret = subprocess_create(command, subprocess_option_enable_async | subprocess_option_combined_stdout_stderr, &process)) != 0) { + running = false; + return; + } + + child = process; + + do { + bytes_read = subprocess_read_stdout(&process, data.data() + index, + sizeof(data) - 1 - index); + + index += bytes_read; + process_log = data.data(); + } while (bytes_read != 0); + + subprocess_join(&process, &ret); + subprocess_destroy(&process); +} + void exec(const std::vector& vec) noexcept { std::int32_t status{}; auto pid = fork(); diff --git a/src/utils.hpp b/src/utils.hpp index 445abc1..3ebfda0 100644 --- a/src/utils.hpp +++ b/src/utils.hpp @@ -2,6 +2,7 @@ #define UTILS_HPP #include "definitions.hpp" +#include "subprocess.h" #include // for from_chars #include // for string @@ -25,6 +26,7 @@ void umount_partitions() noexcept; void find_partitions() noexcept; void arch_chroot(const std::string_view& command) noexcept; +void exec_follow(const std::vector& vec, std::string& process_log, bool& running, subprocess_s& child, bool async = true) noexcept; void exec(const std::vector& vec) noexcept; auto exec(const std::string_view& command, const bool& interactive = false) noexcept(false) -> std::string; [[nodiscard]] bool check_root() noexcept;