From 8903fedfd90ad44b6d0493ca7e7c81b8a3b84485 Mon Sep 17 00:00:00 2001 From: Gabriel Lafond-Thenaille Date: Thu, 6 Feb 2025 10:10:44 +0100 Subject: [PATCH] process: create a vlc_process API * Create an API to spawn a process, control it and communicate with it though pipes. --- include/vlc_process.h | 114 +++++++++++++++++ src/Makefile.am | 10 +- src/libvlccore.sym | 4 + src/meson.build | 3 + src/missing.c | 44 +++++++ src/posix/process.c | 188 +++++++++++++++++++++++++++++ src/win32/process.c | 275 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 635 insertions(+), 3 deletions(-) create mode 100644 include/vlc_process.h create mode 100644 src/posix/process.c create mode 100644 src/win32/process.c diff --git a/include/vlc_process.h b/include/vlc_process.h new file mode 100644 index 0000000000..bd2af1c642 --- /dev/null +++ b/include/vlc_process.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/***************************************************************************** + * vlc_process.h: vlc_process functions + ***************************************************************************** + * Copyright © 2025 Videolabs, VideoLAN and VLC authors + * + * Authors: Gabriel Lafond Thenaille + *****************************************************************************/ + +#ifndef VLC_PROCESS_H +#define VLC_PROCESS_H + +#include + +#include +#include +#include +#include +#include + +/** + * @ingroup misc + * @file + * VLC_PROCESS API + * @defgroup process Process API + * @{ + */ + +/** + * Spawn a new process with input and output redirection. + * + * Creates and starts a new vlc_process for the specified executable path with + * the given arguments. Sets up pipes to allow reading from the process's + * standard output and writing to its standard input. + * + * @param [in] path Path to the executable to run. Must not be NULL. + * @param [in] argc Number of arguments passed to the process (must be + * greater than 0). + * @param [in] argv Array of argument strings (argv[0] must not be NULL). + * + * @return A pointer to the newly created vlc_process structure on + * success, or NULL on failure. + */ +VLC_API struct vlc_process * +vlc_process_Spawn(const char *path, int argc, const char *const *argv); + +/** + * Stop a vlc_process and wait for its termination. + * + * Closes its file descriptors, and waits for it to exit. Optionally sends a + * termination signal to the process, + * + * @param [in] process Pointer to the vlc_process instance. Must not + * be NULL. + * @param [in] kill_process Whether to forcibly terminate the process + * before waiting. + * + * @return The exit status of the process, or -1 on error. + */ +VLC_API int +vlc_process_Terminate(struct vlc_process *process, bool kill_process); + +/** + * Read data from the process's standard output with a timeout. + * + * Attempts to read up to @p size bytes from the process's standard output + * into the provided buffer, waiting up to @p timeout_ms milliseconds for data + * to become available. + * + * On POSIX systems, this uses poll to wait for readability. On Windows, + * a platform-specific implementation is used due to limitations with poll on + * non-socket handles. + * + * @param [in] process Pointer to the vlc_process instance. + * @param [out] buf Buffer where the read data will be stored. + * @param [in] size Maximum number of bytes to read. + * @param [in] timeout_ms Timeout in milliseconds to wait for data. + * + * @return The number of bytes read on success, + * -1 on error, and errno is set to indicate the error. + */ +VLC_API ssize_t +vlc_process_fd_Read(struct vlc_process *process, uint8_t *buf, size_t size, + vlc_tick_t timeout_ms); + +/** + * Write data to the process's standard input with a timeout. + * + * Attempts to write up to @p size bytes from the provided buffer to the + * process's standard input, waiting up to @p timeout_ms milliseconds for the + * pipe to become writable. + * + * On POSIX systems, this uses poll to wait for writability. On Windows, + * a platform-specific implementation is used due to limitations with poll on + * non-socket handles. + * + * @param [in] process Pointer to the vlc_process instance. + * @param [in] buf Buffer containing the data to write. + * @param [in] size Number of bytes to write. + * @param [in] timeout_ms Timeout in milliseconds to wait for the pipe to be + * writable. + * + * @return The number of bytes read on success, + * -1 on error, and errno is set to indicate the error. + */ +VLC_API ssize_t +vlc_process_fd_Write(struct vlc_process *process, const uint8_t *buf, size_t size, + vlc_tick_t timeout_ms); + +/** + * @} process + */ + +#endif /* VLC_PROCESS_H */ diff --git a/src/Makefile.am b/src/Makefile.am index c226f8f57f..a6e510e505 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -94,6 +94,7 @@ pluginsinclude_HEADERS.h = \ ../include/vlc_poll.h \ ../include/vlc_probe.h \ ../include/vlc_preparser.h \ + ../include/vlc_process.h \ ../include/vlc_queue.h \ ../include/vlc_rand.h \ ../include/vlc_renderer_discovery.h \ @@ -448,7 +449,8 @@ libvlccore_la_SOURCES += \ win32/plugin.c \ win32/rand.c \ win32/specific.c \ - win32/thread.c + win32/thread.c \ + win32/process.c if HAVE_WINSTORE libvlccore_la_SOURCES += posix/timer.c win32/dirs-uap.c else @@ -540,7 +542,8 @@ libvlccore_la_SOURCES += \ libvlccore_la_LIBADD += $(LIBEXECINFO) if HAVE_OSX libvlccore_la_SOURCES += \ - posix/spawn.c + posix/spawn.c \ + posix/process.c endif if !HAVE_DARWIN if !HAVE_EMSCRIPTEN @@ -551,7 +554,8 @@ libvlccore_la_SOURCES += \ posix/error.c \ posix/picture.c \ posix/spawn.c \ - posix/specific.c + posix/specific.c \ + posix/process.c if HAVE_LIBANL libvlccore_la_SOURCES += \ linux/getaddrinfo.c diff --git a/src/libvlccore.sym b/src/libvlccore.sym index db396839e6..8ae623db17 100644 --- a/src/libvlccore.sym +++ b/src/libvlccore.sym @@ -1066,3 +1066,7 @@ vlc_preparser_req_GetItem vlc_preparser_req_Release vlc_preparser_Delete vlc_preparser_SetTimeout +vlc_process_Spawn +vlc_process_Terminate +vlc_process_fd_Read +vlc_process_fd_Write diff --git a/src/meson.build b/src/meson.build index 02bb1a1f29..700b839ed9 100644 --- a/src/meson.build +++ b/src/meson.build @@ -330,6 +330,7 @@ if host_system == 'darwin' if have_osx libvlccore_sources += [ 'posix/spawn.c', + 'posix/process.c', ] endif elif host_system == 'windows' @@ -353,6 +354,7 @@ elif host_system == 'windows' 'win32/timer.c', 'win32/dirs.c', 'win32/spawn.c', + 'win32/process.c', ] endif else @@ -372,6 +374,7 @@ else 'posix/picture.c', 'posix/specific.c', 'posix/thread.c', + 'posix/process.c', ] endif diff --git a/src/missing.c b/src/missing.c index 641f799aab..157a2236d9 100644 --- a/src/missing.c +++ b/src/missing.c @@ -37,6 +37,8 @@ #include #include +#include + #ifndef ENABLE_VLM # include @@ -180,4 +182,46 @@ int vlc_waitpid(pid_t pid) (void) pid; vlc_assert_unreachable(); } + +VLC_WEAK struct vlc_process * +vlc_process_Spawn(const char *path, int argc, const char *const *argv) +{ + VLC_UNUSED(path); + VLC_UNUSED(argc); + VLC_UNUSED(argv); + return NULL; +} + +VLC_WEAK int +vlc_process_Terminate(struct vlc_process *process, bool kill_process) +{ + VLC_UNUSED(process); + VLC_UNUSED(kill_process); + vlc_assert_unreachable(); + return -1; +} + +VLC_WEAK ssize_t +vlc_process_fd_Read(struct vlc_process *process, uint8_t *buf, size_t size, + vlc_tick_t timeout_ms) +{ + VLC_UNUSED(process); + VLC_UNUSED(buf); + VLC_UNUSED(size); + VLC_UNUSED(timeout_ms); + vlc_assert_unreachable(); + return -1; +} + +VLC_WEAK ssize_t +vlc_process_fd_Write(struct vlc_process *process, const uint8_t *buf, size_t size, + vlc_tick_t timeout_ms) +{ + VLC_UNUSED(process); + VLC_UNUSED(buf); + VLC_UNUSED(size); + VLC_UNUSED(timeout_ms); + vlc_assert_unreachable(); + return -1; +} #endif diff --git a/src/posix/process.c b/src/posix/process.c new file mode 100644 index 0000000000..d1c61f7297 --- /dev/null +++ b/src/posix/process.c @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/***************************************************************************** + * process.c: posix implementation of process management + ***************************************************************************** + * Copyright © 2025 Videolabs, VideoLAN and VLC authors + * + * Authors: Gabriel Lafond Thenaille + *****************************************************************************/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#ifdef HAVE_POLL_H +# include +#endif + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +struct vlc_process { + + /* Pid of the linked process */ + pid_t pid; + + /* File descriptor of the socketpair */ + int fd; +}; + +struct vlc_process* +vlc_process_Spawn(const char *path, int argc, const char *const *argv) +{ + assert(path != NULL); + assert(argc > 0); + assert(argv != NULL); + assert(argv[0] != NULL); + + int ret = VLC_EGENERIC; + int fds[2] = { -1, -1 }; + int extfd_in = -1; + int extfd_out = -1; + const char **args = NULL; + + struct vlc_process *process = malloc(sizeof(*process)); + if (process == NULL) { + return NULL; + } + + ret = vlc_socketpair(PF_LOCAL, SOCK_STREAM, 0, fds, false); + if (ret != 0) { + goto end; + } + + extfd_in = vlc_dup(fds[1]); + if (extfd_in == -1) { + ret = -1; + goto end; + } + extfd_out = vlc_dup(fds[1]); + if (extfd_out == -1) { + ret = -1; + goto end; + } + + process->fd = fds[0]; + + int process_fds[4] = {extfd_in, extfd_out, STDERR_FILENO, -1}; + + /* `argc + 2`, 1 for the process->path and the last to be NULL */ + args = malloc((argc + 2) * sizeof(*args)); + if (args == NULL) { + ret = VLC_ENOMEM; + goto end; + } + args[0] = path; + for (int i = 0; i < argc; i++) { + args[i + 1] = argv[i]; + } + args[argc + 1] = NULL; + + ret = vlc_spawnp(&process->pid, path, process_fds, args); + if (ret != 0) { + goto end; + } + +end: + + free(args); + + if (extfd_in != -1) { + vlc_close(extfd_in); + } + if (extfd_out != -1) { + vlc_close(extfd_out); + } + if (fds[1] != -1) { + net_Close(fds[1]); + } + + if (ret != 0) { + if (fds[0] != -1) { + shutdown(fds[0], SHUT_RDWR); + net_Close(fds[0]); + } + free(process); + return NULL; + } + return process; +} + +VLC_API int +vlc_process_Terminate(struct vlc_process *process, bool kill_process) +{ + assert(process != NULL); + + if (kill_process) { + kill(process->pid, SIGTERM); + } + + shutdown(process->fd, SHUT_RDWR); + net_Close(process->fd); + + int status = vlc_waitpid(process->pid); + process->pid = 0; + process->fd = -1; + free(process); + return status; +} + +ssize_t +vlc_process_fd_Read(struct vlc_process *process, uint8_t *buf, size_t size, + vlc_tick_t timeout_ms) +{ + assert(process != NULL); + assert(process->fd != -1); + assert(buf != NULL); + + struct pollfd fds = { + .fd = process->fd, .events = POLLIN, .revents = 0 + }; + + int ret = vlc_poll_i11e(&fds, 1, timeout_ms); + if (ret < 0) { + return -1; + } else if (ret == 0) { + errno = ETIMEDOUT; + return -1; + } else if (!(fds.revents & POLLIN)) { + errno = EINVAL; + return -1; + } + + return recv(process->fd, buf, size, 0); +} + +ssize_t +vlc_process_fd_Write(struct vlc_process *process, const uint8_t *buf, + size_t size, vlc_tick_t timeout_ms) +{ + assert(process != NULL); + assert(process->fd != -1); + assert(buf != NULL); + + struct pollfd fds = { + .fd = process->fd, .events = POLLOUT, .revents = 0 + }; + + int ret = vlc_poll_i11e(&fds, 1, timeout_ms); + if (ret < 0) { + return -1; + } else if (ret == 0) { + errno = ETIMEDOUT; + return -1; + } else if (!(fds.revents & POLLOUT)) { + errno = EINVAL; + return -1; + } + + return vlc_send_i11e(process->fd, buf, size, 0); +} diff --git a/src/win32/process.c b/src/win32/process.c new file mode 100644 index 0000000000..033dcc759d --- /dev/null +++ b/src/win32/process.c @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/***************************************************************************** + * process.c: win32 implementation of process management + ***************************************************************************** + * Copyright © 2025 Videolabs, VideoLAN and VLC authors + * + * Authors: Gabriel Lafond Thenaille + *****************************************************************************/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include + +#include +#include +#include +#include +#include + +static void CALLBACK +vlc_process_WindowsPoll_i11e_wake_self(ULONG_PTR data) +{ + (void) data; +} + +static void +vlc_process_WindowsPoll_i11e_wake(void *opaque) +{ + assert(opaque != NULL); + + HANDLE th = opaque; + QueueUserAPC(vlc_process_WindowsPoll_i11e_wake_self, th, 0); +} + +static int +vlc_process_WindowsPoll(HANDLE hFd, LPOVERLAPPED lpoverlapped, DWORD *bytes, + vlc_tick_t timeout_ms) +{ + HANDLE th; + if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), + GetCurrentProcess(), &th, 0, FALSE, + DUPLICATE_SAME_ACCESS)) { + return ENOMEM; + } + vlc_interrupt_register(vlc_process_WindowsPoll_i11e_wake, th); + DWORD waitResult = WaitForSingleObjectEx(lpoverlapped->hEvent, + timeout_ms, TRUE); + vlc_interrupt_unregister(); + CloseHandle(th); + switch (waitResult) { + case WAIT_OBJECT_0: + if (GetOverlappedResult(hFd, lpoverlapped, bytes, FALSE)) { + return VLC_SUCCESS; + } else { + return EINVAL; + } + case WAIT_TIMEOUT: + /* Timeout occurred */ + CancelIo(hFd); /* Cancel the I/O operation */ + return ETIMEDOUT; + case WAIT_IO_COMPLETION: + /* Interrupt occurred */ + CancelIo(hFd); /* Cancel the I/O operation */ + return EINTR; + default: + return EINVAL; + } +} + +struct vlc_process { + /* Pid of the linked process */ + pid_t pid; + + int fd_in; + int fd_out; + + HANDLE hEvent; +}; + +struct vlc_process* +vlc_process_Spawn(const char *path, int argc, const char *const *argv) +{ + assert(path != NULL); + if (argc > 0) { + assert(argv != NULL); + assert(argv[0] != NULL); + } + + int ret = VLC_EGENERIC; + int fds[2] = { -1, -1 }; + int extfd_in = -1; + int extfd_out = -1; + const char **args = NULL; + + struct vlc_process *process = malloc(sizeof(*process)); + if (process == NULL) { + return NULL; + } + + process->fd_in = -1; + process->fd_out = -1; + process->hEvent = INVALID_HANDLE_VALUE; + + ret = vlc_pipe(fds); + if (ret != 0) { + goto end; + } + extfd_out = fds[1]; + process->fd_in = fds[0]; + + ret = vlc_pipe(fds); + if (ret != 0) { + goto end; + } + extfd_in = fds[0]; + process->fd_out = fds[1]; + + process->hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (process->hEvent == INVALID_HANDLE_VALUE) { + errno = EINVAL; + goto end; + } + + int process_fds[4] = {extfd_in, extfd_out, STDERR_FILENO, -1}; + + /* `argc + 2`, 1 for the process->path and the last to be NULL */ + args = malloc((argc + 2) * sizeof(*args)); + if (args == NULL) { + ret = VLC_ENOMEM; + goto end; + } + args[0] = path; + for (int i = 0; i < argc; i++) { + args[i + 1] = argv[i]; + } + args[argc + 1] = NULL; + + ret = vlc_spawnp(&process->pid, path, process_fds, args); + if (ret != 0) { + goto end; + } + +end: + + free(args); + + if (extfd_in != -1) { + vlc_close(extfd_in); + } + if (extfd_out != -1) { + vlc_close(extfd_out); + } + + if (ret != 0) { + if (process->fd_in != -1) { + vlc_close(process->fd_in); + } + if (process->fd_out != -1) { + vlc_close(process->fd_out); + } + if (process->hEvent != INVALID_HANDLE_VALUE) { + CloseHandle(process->hEvent); + } + free(process); + return NULL; + } + return process; +} + +int +vlc_process_Terminate(struct vlc_process *process, bool kill_process) +{ + assert(process != NULL); + + if (kill_process) { + HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, process->pid); + if (hProcess) { + TerminateProcess(hProcess, 15); + CloseHandle(hProcess); + } + } + + vlc_close(process->fd_in); + vlc_close(process->fd_out); + CloseHandle(process->hEvent); + + int status = vlc_waitpid(process->pid); + process->pid = 0; + free(process); + return status; +} + +ssize_t +vlc_process_fd_Read(struct vlc_process *process, uint8_t *buf, size_t size, + vlc_tick_t timeout_ms) +{ + assert(process != NULL); + assert(buf != NULL); + + intptr_t h = _get_osfhandle(process->fd_in); + if (h == -1) { + errno = EINVAL; + return -1; + } + HANDLE hFd = (HANDLE)h; + + DWORD bytes = 0; + OVERLAPPED overlapped = {0}; + overlapped.hEvent = process->hEvent; + + BOOL ret = FALSE; + ret = ReadFile(hFd, buf, size, &bytes, &overlapped); + + int err = VLC_SUCCESS; + + if (ret) { + return bytes; + } else { + DWORD error = GetLastError(); + if (error == ERROR_IO_PENDING) { + err = vlc_process_WindowsPoll(hFd, &overlapped, &bytes, + timeout_ms); + } else { + err = EINVAL; + } + } + if (err == VLC_SUCCESS) { + return bytes; + } + errno = err; + return -1; +} + +ssize_t +vlc_process_fd_Write(struct vlc_process *process, const uint8_t *buf, size_t size, + vlc_tick_t timeout_ms) +{ + assert(process != NULL); + assert(buf != NULL); + + intptr_t h = _get_osfhandle(process->fd_out); + if (h == -1) { + errno = EINVAL; + return -1; + } + HANDLE hFd = (HANDLE)h; + + DWORD bytes = 0; + OVERLAPPED overlapped = {0}; + overlapped.hEvent = process->hEvent; + + BOOL ret = FALSE; + ret = WriteFile(hFd, buf, size, &bytes, &overlapped); + + int err = VLC_SUCCESS; + + if (ret) { + return bytes; + } else { + DWORD error = GetLastError(); + if (error == ERROR_IO_PENDING) { + err = vlc_process_WindowsPoll(hFd, &overlapped, &bytes, + timeout_ms); + } else { + err = EINVAL; + } + } + if (err == VLC_SUCCESS) { + return bytes; + } + errno = err; + return -1; +}