diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index f8d5db678..b0f4fa65f 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -341,6 +341,8 @@ C1077A51183EB29600634C22 /* file.h in Headers */ = {isa = PBXBuildFile; fileRef = C1077A4D183EB29600634C22 /* file.h */; }; C10C644D1D9AF328003C1B4C /* session-id.c in Sources */ = {isa = PBXBuildFile; fileRef = C10C644B1D9AF328003C1B4C /* session-id.c */; }; C10C644E1D9AF328003C1B4C /* session-id.h in Headers */ = {isa = PBXBuildFile; fileRef = C10C644C1D9AF328003C1B4C /* session-id.h */; }; + C11DEA161FCD31C0009E22B9 /* subprocess-posix.c in Sources */ = {isa = PBXBuildFile; fileRef = C11DEA141FCD31C0009E22B9 /* subprocess-posix.c */; }; + C11DEA171FCD31C0009E22B9 /* subprocess.h in Headers */ = {isa = PBXBuildFile; fileRef = C11DEA151FCD31C0009E22B9 /* subprocess.h */; }; C12F19791E1AE3C30005E93F /* upnperrors.c in Sources */ = {isa = PBXBuildFile; fileRef = C12F19771E1AE3C30005E93F /* upnperrors.c */; }; C12F197B1E1AE4460005E93F /* upnperrors.h in Headers */ = {isa = PBXBuildFile; fileRef = C12F197A1E1AE4460005E93F /* upnperrors.h */; }; C1305EBE186A13B100F03351 /* file.c in Sources */ = {isa = PBXBuildFile; fileRef = C1305EB8186A134000F03351 /* file.c */; }; @@ -987,6 +989,8 @@ C1077A4D183EB29600634C22 /* file.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = file.h; path = libtransmission/file.h; sourceTree = ""; }; C10C644B1D9AF328003C1B4C /* session-id.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "session-id.c"; path = "libtransmission/session-id.c"; sourceTree = ""; }; C10C644C1D9AF328003C1B4C /* session-id.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "session-id.h"; path = "libtransmission/session-id.h"; sourceTree = ""; }; + C11DEA141FCD31C0009E22B9 /* subprocess-posix.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "subprocess-posix.c"; path = "libtransmission/subprocess-posix.c"; sourceTree = ""; }; + C11DEA151FCD31C0009E22B9 /* subprocess.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = subprocess.h; path = libtransmission/subprocess.h; sourceTree = ""; }; C12F19771E1AE3C30005E93F /* upnperrors.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = upnperrors.c; path = "third-party/miniupnpc/upnperrors.c"; sourceTree = ""; }; C12F197A1E1AE4460005E93F /* upnperrors.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = upnperrors.h; path = "third-party/miniupnpc/upnperrors.h"; sourceTree = ""; }; C1305EB8186A134000F03351 /* file.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = file.c; path = libtransmission/file.c; sourceTree = ""; }; @@ -1357,6 +1361,8 @@ C1077A4C183EB29600634C22 /* file-posix.c */, C1305EB8186A134000F03351 /* file.c */, C1077A4D183EB29600634C22 /* file.h */, + C11DEA141FCD31C0009E22B9 /* subprocess-posix.c */, + C11DEA151FCD31C0009E22B9 /* subprocess.h */, 4D80185710BBC0B0008A4AF2 /* magnet.c */, 4D80185810BBC0B0008A4AF2 /* magnet.h */, 4D8017E810BBC073008A4AF2 /* torrent-magnet.c */, @@ -1810,6 +1816,7 @@ 4D36BA780CA2F00800A63CA5 /* peer-mgr.h in Headers */, 4D36BA7A0CA2F00800A63CA5 /* peer-msgs.h in Headers */, 4D36BA7B0CA2F00800A63CA5 /* ptrarray.h in Headers */, + C11DEA171FCD31C0009E22B9 /* subprocess.h in Headers */, A25D2CBE0CF4C73E0096A262 /* stats.h in Headers */, C1033E0A1A3279B800EF44D8 /* crypto-utils.h in Headers */, A29DF8BA0DB2544C00D04E5A /* resume.h in Headers */, @@ -2358,6 +2365,7 @@ D4AF3B2F0C41F7A500D46B6B /* list.c in Sources */, 4394AC670C74FB6000F367E8 /* ptrarray.c in Sources */, A24621420C769D0900088E81 /* trevent.c in Sources */, + C11DEA161FCD31C0009E22B9 /* subprocess-posix.c in Sources */, 4D36BA6F0CA2F00800A63CA5 /* crypto.c in Sources */, 4D36BA720CA2F00800A63CA5 /* handshake.c in Sources */, 4D36BA740CA2F00800A63CA5 /* peer-io.c in Sources */, diff --git a/libtransmission/CMakeLists.txt b/libtransmission/CMakeLists.txt index 99854b8ff..96d73ad90 100644 --- a/libtransmission/CMakeLists.txt +++ b/libtransmission/CMakeLists.txt @@ -47,6 +47,8 @@ set(${PROJECT_NAME}_SOURCES rpc-server.c session.c session-id.c + subprocess-posix.c + subprocess-win32.c stats.c torrent.c torrent-ctor.c @@ -94,9 +96,9 @@ else() endif() if(WIN32) - set_source_files_properties(file-posix.c PROPERTIES HEADER_FILE_ONLY ON) + set_source_files_properties(file-posix.c subprocess-posix.c PROPERTIES HEADER_FILE_ONLY ON) else() - set_source_files_properties(file-win32.c watchdir-win32.c PROPERTIES HEADER_FILE_ONLY ON) + set_source_files_properties(file-win32.c subprocess-win32.c watchdir-win32.c PROPERTIES HEADER_FILE_ONLY ON) endif() set(${PROJECT_NAME}_PUBLIC_HEADERS @@ -152,6 +154,7 @@ set(${PROJECT_NAME}_PRIVATE_HEADERS resume.h rpc-server.h session.h + subprocess.h stats.h torrent.h torrent-magnet.h @@ -275,11 +278,12 @@ if(ENABLE_TESTS) set_property(TARGET ${TR_NAME}-test PROPERTY FOLDER "UnitTests") set(crypto-test_ADD_SOURCES crypto-test-ref.h) + set(subprocess-test_ADD_SOURCES subprocess-test.cmd) set(watchdir@generic-test_DEFINITIONS WATCHDIR_TEST_FORCE_GENERIC) foreach(T bitfield blocklist clients crypto error file history json magnet makemeta metainfo move peer-msgs quark rename rpc - session tr-getopt utils variant watchdir watchdir@generic) + session subprocess tr-getopt utils variant watchdir watchdir@generic) set(TP ${TR_NAME}-test-${T}) if(T MATCHES "^([^@]+)@.+$") string(REPLACE "@" "_" TP "${TP}") @@ -295,6 +299,12 @@ if(ENABLE_TESTS) add_test(NAME ${T} COMMAND ${TP}) set_property(TARGET ${TP} PROPERTY FOLDER "UnitTests") endforeach() + + if(WIN32) + add_custom_command(TARGET ${TR_NAME}-test-subprocess PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_SOURCE_DIR}/subprocess-test.cmd + $/${TR_NAME}-test-subprocess.cmd) + endif() endif() if(INSTALL_LIB) diff --git a/libtransmission/Makefile.am b/libtransmission/Makefile.am index dfb66c7a9..0885fab0d 100644 --- a/libtransmission/Makefile.am +++ b/libtransmission/Makefile.am @@ -91,9 +91,9 @@ AM_CPPFLAGS += -DWITH_KQUEUE endif if WIN32 -libtransmission_a_SOURCES += file-win32.c watchdir-win32.c +libtransmission_a_SOURCES += file-win32.c subprocess-win32.c watchdir-win32.c else -libtransmission_a_SOURCES += file-posix.c +libtransmission_a_SOURCES += file-posix.c subprocess-posix.c endif if CRYPTO_USE_OPENSSL @@ -157,6 +157,7 @@ noinst_HEADERS = \ session.h \ session-id.h \ stats.h \ + subprocess.h \ torrent.h \ torrent-magnet.h \ tr-getopt.h \ @@ -197,6 +198,7 @@ TESTS = \ rename-test \ rpc-test \ session-test \ + subprocess-test \ tr-getopt-test \ utils-test \ variant-test \ @@ -286,6 +288,10 @@ session_test_SOURCES = session-test.c $(TEST_SOURCES) session_test_LDADD = ${apps_ldadd} session_test_LDFLAGS = ${apps_ldflags} +subprocess_test_SOURCES = subprocess-test.c $(TEST_SOURCES) +subprocess_test_LDADD = ${apps_ldadd} +subprocess_test_LDFLAGS = ${apps_ldflags} + tr_getopt_test_SOURCES = tr-getopt-test.c $(TEST_SOURCES) tr_getopt_test_LDADD = ${apps_ldadd} tr_getopt_test_LDFLAGS = ${apps_ldflags} diff --git a/libtransmission/file-posix.c b/libtransmission/file-posix.c index b48228935..8bd434f16 100644 --- a/libtransmission/file-posix.c +++ b/libtransmission/file-posix.c @@ -1,5 +1,5 @@ /* - * This file Copyright (C) 2013-2014 Mnemosyne LLC + * This file Copyright (C) 2013-2017 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. @@ -444,6 +444,11 @@ bool tr_sys_path_remove(char const* path, tr_error** error) return ret; } +char* tr_sys_path_native_separators(char* path) +{ + return path; +} + tr_sys_file_t tr_sys_file_get_std(tr_std_sys_file_t std_file, tr_error** error) { tr_sys_file_t ret = TR_BAD_SYS_FILE; diff --git a/libtransmission/file-test.c b/libtransmission/file-test.c index 79a0064a3..25aafed30 100644 --- a/libtransmission/file-test.c +++ b/libtransmission/file-test.c @@ -1,5 +1,5 @@ /* - * This file Copyright (C) 2013-2014 Mnemosyne LLC + * This file Copyright (C) 2013-2017 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. @@ -959,6 +959,42 @@ static int test_path_remove(void) return 0; } +static int test_path_native_separators(void) +{ + check_str(tr_sys_path_native_separators(NULL), == , NULL); + + char path1[] = ""; + char path2[] = "a"; + char path3[] = "/"; + char path4[] = "/a/b/c"; + char path5[] = "C:\\a/b\\c"; + + struct + { + char* input; + char const* output; + } + test_data[] = + { + { path1, "" }, + { path2, TR_IF_WIN32("a", "a") }, + { path3, TR_IF_WIN32("\\", "/") }, + { path4, TR_IF_WIN32("\\a\\b\\c", "/a/b/c") }, + { path5, TR_IF_WIN32("C:\\a\\b\\c", "C:\\a/b\\c") }, + }; + + for (size_t i = 0; i < TR_N_ELEMENTS(test_data); ++i) + { + char* const output = tr_sys_path_native_separators(test_data[i].input); + + check_str(output, ==, test_data[i].output); + check_str(test_data[i].input, ==, test_data[i].output); + check_ptr(output, ==, test_data[i].input); + } + + return 0; +} + static int test_file_open(void) { char* const test_dir = create_test_dir(__FUNCTION__); @@ -1565,6 +1601,7 @@ int main(void) test_path_basename_dirname, test_path_rename, test_path_remove, + test_path_native_separators, test_file_open, test_file_read_write_seek, test_file_truncate, diff --git a/libtransmission/file-win32.c b/libtransmission/file-win32.c index 448fa3f40..30acebd46 100644 --- a/libtransmission/file-win32.c +++ b/libtransmission/file-win32.c @@ -1,5 +1,5 @@ /* - * This file Copyright (C) 2013-2014 Mnemosyne LLC + * This file Copyright (C) 2013-2017 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. @@ -742,6 +742,21 @@ bool tr_sys_path_remove(char const* path, tr_error** error) return ret; } +char* tr_sys_path_native_separators(char* path) +{ + if (path == NULL) + { + return NULL; + } + + for (char* slash = strchr(path, '/'); slash != NULL; slash = strchr(slash, '/')) + { + *slash = '\\'; + } + + return path; +} + tr_sys_file_t tr_sys_file_get_std(tr_std_sys_file_t std_file, tr_error** error) { tr_sys_file_t ret = TR_BAD_SYS_FILE; diff --git a/libtransmission/file.h b/libtransmission/file.h index a878c8e7c..d723d65e8 100644 --- a/libtransmission/file.h +++ b/libtransmission/file.h @@ -1,5 +1,5 @@ /* - * This file Copyright (C) 2013-2014 Mnemosyne LLC + * This file Copyright (C) 2013-2017 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. @@ -266,6 +266,15 @@ bool tr_sys_path_rename(char const* src_path, char const* dst_path, struct tr_er */ bool tr_sys_path_remove(char const* path, struct tr_error** error); +/** + * @brief Transform path separators to native ones, in-place. + * + * @param[in,out] path Path to transform. + * + * @return Same path but with native (and uniform) separators. + */ +char* tr_sys_path_native_separators(char* path); + /* File-related wrappers */ /** diff --git a/libtransmission/libtransmission-test.c b/libtransmission/libtransmission-test.c index dbca07aad..bc4888cd4 100644 --- a/libtransmission/libtransmission-test.c +++ b/libtransmission/libtransmission-test.c @@ -1,5 +1,5 @@ /* - * This file Copyright (C) 2013-2014 Mnemosyne LLC + * This file Copyright (C) 2013-2017 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. @@ -208,7 +208,7 @@ char* libtest_sandbox_create(void) char* sandbox = tr_buildPath(path, "sandbox-XXXXXX", NULL); tr_free(path); tr_sys_dir_create_temp(sandbox, NULL); - return sandbox; + return tr_sys_path_native_separators(sandbox); } static void rm_rf(char const* killme) diff --git a/libtransmission/subprocess-posix.c b/libtransmission/subprocess-posix.c new file mode 100644 index 000000000..840dbd046 --- /dev/null +++ b/libtransmission/subprocess-posix.c @@ -0,0 +1,173 @@ +/* + * This file Copyright (C) 2010-2017 Mnemosyne LLC + * + * It may be used under the GNU GPL versions 2 or 3 + * or any future license endorsed by Mnemosyne LLC. + * + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "transmission.h" +#include "error.h" +#include "subprocess.h" +#include "tr-assert.h" +#include "tr-macros.h" +#include "utils.h" + +static void handle_sigchld(int i UNUSED) +{ + int rc; + + do + { + /* FIXME: Only check for our own PIDs */ + rc = waitpid(-1, NULL, WNOHANG); + } + while (rc > 0 || (rc == -1 && errno == EINTR)); + + /* FIXME: Call old handler, if any */ +} + +static void set_system_error(tr_error** error, int code, char const* what) +{ + if (error == NULL) + { + return; + } + + if (what == NULL) + { + tr_error_set_literal(error, code, tr_strerror(code)); + } + else + { + tr_error_set(error, code, "%s failed: %s", what, tr_strerror(code)); + } +} + +static bool tr_spawn_async_in_child(char* const* cmd, char* const* env, char const* work_dir, int pipe_fd) +{ + if (env != NULL) + { + for (size_t i = 0; env[i] != NULL; ++i) + { + if (putenv(env[i]) != 0) + { + goto fail; + } + } + } + + if (work_dir != NULL && chdir(work_dir) == -1) + { + goto fail; + } + + if (execvp(cmd[0], cmd) == -1) + { + goto fail; + } + + return true; + +fail: + write(pipe_fd, &errno, sizeof(errno)); + return false; +} + +static bool tr_spawn_async_in_parent(int pipe_fd, tr_error** error) +{ + int child_errno; + ssize_t count; + + TR_STATIC_ASSERT(sizeof(child_errno) == sizeof(errno), ""); + + do + { + count = read(pipe_fd, &child_errno, sizeof(child_errno)); + } + while (count == -1 && errno == EINTR); + + close(pipe_fd); + + if (count == -1) + { + /* Read failed (what to do?) */ + } + else if (count == 0) + { + /* Child successfully exec-ed */ + } + else + { + TR_ASSERT((size_t)count == sizeof(child_errno)); + + set_system_error(error, child_errno, "Child process setup"); + return false; + } + + return true; +} + +bool tr_spawn_async(char* const* cmd, char* const* env, char const* work_dir, tr_error** error) +{ + static bool sigchld_handler_set = false; + + if (!sigchld_handler_set) + { + /* FIXME: "The effects of signal() in a multithreaded process are unspecified." (c) man 2 signal */ + if (signal(SIGCHLD, &handle_sigchld) == SIG_ERR) + { + set_system_error(error, errno, "Call to signal()"); + return false; + } + + sigchld_handler_set = true; + } + + int pipe_fds[2]; + + if (pipe(pipe_fds) == -1) + { + set_system_error(error, errno, "Call to pipe()"); + return false; + } + + if (fcntl(pipe_fds[1], F_SETFD, fcntl(pipe_fds[1], F_GETFD) | FD_CLOEXEC)) + { + set_system_error(error, errno, "Call to fcntl()"); + close(pipe_fds[0]); + close(pipe_fds[1]); + return false; + } + + int const child_pid = fork(); + + if (child_pid == -1) + { + set_system_error(error, errno, "Call to fork()"); + return false; + } + + if (child_pid == 0) + { + close(pipe_fds[0]); + + if (!tr_spawn_async_in_child(cmd, env, work_dir, pipe_fds[1])) + { + _exit(0); + } + } + + close(pipe_fds[1]); + + return tr_spawn_async_in_parent(pipe_fds[0], error); +} diff --git a/libtransmission/subprocess-test.c b/libtransmission/subprocess-test.c new file mode 100644 index 000000000..231cda637 --- /dev/null +++ b/libtransmission/subprocess-test.c @@ -0,0 +1,392 @@ +/* + * This file Copyright (C) 2017 Mnemosyne LLC + * + * It may be used under the GNU GPL versions 2 or 3 + * or any future license endorsed by Mnemosyne LLC. + * + */ + +#include + +#include "transmission.h" +#include "error.h" +#include "file.h" +#include "subprocess.h" +#include "utils.h" + +#include "libtransmission-test.h" + +static char arg_dump_args[] = "--dump-args"; +static char arg_dump_env[] = "--dump-env"; +static char arg_dump_cwd[] = "--dump-cwd"; + +static char* self_path = NULL; + +static int test_spawn_async_missing_exe(void) +{ + char missing_exe_path[] = TR_IF_WIN32("C:\\", "/") "tr-missing-test-exe" TR_IF_WIN32(".exe", ""); + + char* const args[] = + { + missing_exe_path, + NULL + }; + + tr_error* error = NULL; + bool const ret = tr_spawn_async(args, NULL, NULL, &error); + check_bool(ret, == , false); + check_ptr(error, != , NULL); + check_int(error->code, != , 0); + check_str(error->message, != , NULL); + + return 0; +} + +static int test_spawn_async_args(void) +{ + char* const test_dir = libtest_sandbox_create(); + char* const result_path = tr_sys_path_native_separators(tr_buildPath(test_dir, "result.txt", NULL)); + bool const allow_batch_metachars = TR_IF_WIN32(false, true) || !tr_str_has_suffix(self_path, ".cmd"); + + char test_arg_1[] = "arg1 "; + char test_arg_2[] = " arg2"; + char test_arg_3[] = ""; + char test_arg_4[] = "\"arg3'^! $PATH %PATH% \\"; + + char* const args[] = + { + self_path, + result_path, + arg_dump_args, + test_arg_1, + test_arg_2, + test_arg_3, + allow_batch_metachars ? test_arg_4 : NULL, + NULL + }; + + tr_error* error = NULL; + bool const ret = tr_spawn_async(args, NULL, NULL, &error); + check_bool(ret, ==, true); + check_ptr(error, ==, NULL); + + while (!tr_sys_path_exists(result_path, NULL)) + { + tr_wait_msec(10); + } + + tr_sys_file_t fd = tr_sys_file_open(result_path, TR_SYS_FILE_READ, 0, NULL); + check_int(fd, !=, TR_BAD_SYS_FILE); + + char buffer[1024]; + + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(buffer, ==, test_arg_1); + + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(buffer, ==, test_arg_2); + + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(buffer, == , test_arg_3); + + if (allow_batch_metachars) + { + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(buffer, == , test_arg_4); + } + + check(!tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + + tr_sys_file_close(fd, NULL); + + tr_free(result_path); + libtest_sandbox_destroy(test_dir); + tr_free(test_dir); + return 0; +} + +static int test_spawn_async_env(void) +{ + char* const test_dir = libtest_sandbox_create(); + char* const result_path = tr_sys_path_native_separators(tr_buildPath(test_dir, "result.txt", NULL)); + + char test_env_key_1[] = "VAR1"; + char test_env_key_2[] = "_VAR_2_"; + char test_env_key_3[] = "vAr#"; + char test_env_key_4[] = "FOO"; + char test_env_key_5[] = "ZOO"; + char test_env_key_6[] = "TR_MISSING_TEST_ENV_KEY"; + + char test_env_value_1[] = "value1 "; + char test_env_value_2[] = " value2"; + char test_env_value_3[] = " \"value3'^! $PATH %PATH% "; + char test_env_value_4[] = "bar"; + char test_env_value_5[] = "jar"; + + char* const args[] = + { + self_path, + result_path, + arg_dump_env, + test_env_key_1, + test_env_key_2, + test_env_key_3, + test_env_key_4, + test_env_key_5, + test_env_key_6, + NULL + }; + + char* const env[] = + { + tr_strdup_printf("%s=%s", test_env_key_1, test_env_value_1), + tr_strdup_printf("%s=%s", test_env_key_2, test_env_value_2), + tr_strdup_printf("%s=%s", test_env_key_3, test_env_value_3), + tr_strdup_printf("%s=%s", test_env_key_5, test_env_value_5), + NULL + }; + + /* Inherited */ + char foo_env_value[] = "FOO=bar"; + putenv(foo_env_value); + + /* Overridden */ + char zoo_env_value[] = "ZOO=tar"; + putenv(zoo_env_value); + + tr_error* error = NULL; + bool const ret = tr_spawn_async(args, env, NULL, &error); + check_bool(ret, ==, true); + check_ptr(error, ==, NULL); + + while (!tr_sys_path_exists(result_path, NULL)) + { + tr_wait_msec(10); + } + + tr_sys_file_t fd = tr_sys_file_open(result_path, TR_SYS_FILE_READ, 0, NULL); + check_int(fd, !=, TR_BAD_SYS_FILE); + + char buffer[1024]; + + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(buffer, ==, test_env_value_1); + tr_free(env[0]); + + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(buffer, ==, test_env_value_2); + tr_free(env[1]); + + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(buffer, ==, test_env_value_3); + tr_free(env[2]); + + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(buffer, ==, test_env_value_4); + + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(buffer, ==, test_env_value_5); + + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(buffer, ==, ""); + + check(!tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + + tr_sys_file_close(fd, NULL); + + tr_free(result_path); + libtest_sandbox_destroy(test_dir); + tr_free(test_dir); + return 0; +} + +static int test_spawn_async_cwd_explicit(void) +{ + char* const test_dir = libtest_sandbox_create(); + char* const result_path = tr_sys_path_native_separators(tr_buildPath(test_dir, "result.txt", NULL)); + + char* const args[] = + { + self_path, + result_path, + arg_dump_cwd, + NULL + }; + + tr_error* error = NULL; + bool const ret = tr_spawn_async(args, NULL, test_dir, &error); + check_bool(ret, ==, true); + check_ptr(error, ==, NULL); + + while (!tr_sys_path_exists(result_path, NULL)) + { + tr_wait_msec(10); + } + + tr_sys_file_t fd = tr_sys_file_open(result_path, TR_SYS_FILE_READ, 0, NULL); + check_int(fd, !=, TR_BAD_SYS_FILE); + + char buffer[1024]; + + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(tr_sys_path_native_separators(buffer), ==, tr_sys_path_native_separators(test_dir)); + + check(!tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + + tr_sys_file_close(fd, NULL); + + tr_free(result_path); + libtest_sandbox_destroy(test_dir); + tr_free(test_dir); + return 0; +} + +static int test_spawn_async_cwd_inherit(void) +{ + char* const test_dir = libtest_sandbox_create(); + char* const result_path = tr_sys_path_native_separators(tr_buildPath(test_dir, "result.txt", NULL)); + + char* const expected_cwd = tr_sys_dir_get_current(NULL); + + char* const args[] = + { + self_path, + result_path, + arg_dump_cwd, + NULL + }; + + tr_error* error = NULL; + bool const ret = tr_spawn_async(args, NULL, NULL, &error); + check_bool(ret, ==, true); + check_ptr(error, ==, NULL); + + while (!tr_sys_path_exists(result_path, NULL)) + { + tr_wait_msec(10); + } + + tr_sys_file_t fd = tr_sys_file_open(result_path, TR_SYS_FILE_READ, 0, NULL); + check_int(fd, !=, TR_BAD_SYS_FILE); + + char buffer[1024]; + + check(tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + check_str(tr_sys_path_native_separators(buffer), ==, tr_sys_path_native_separators(expected_cwd)); + + check(!tr_sys_file_read_line(fd, buffer, sizeof(buffer), NULL)); + + tr_sys_file_close(fd, NULL); + + tr_free(expected_cwd); + tr_free(result_path); + libtest_sandbox_destroy(test_dir); + tr_free(test_dir); + return 0; +} + +static int test_spawn_async_cwd_missing(void) +{ + char* const test_dir = libtest_sandbox_create(); + char* const result_path = tr_sys_path_native_separators(tr_buildPath(test_dir, "result.txt", NULL)); + + char* const args[] = + { + self_path, + result_path, + arg_dump_cwd, + NULL + }; + + tr_error* error = NULL; + bool const ret = tr_spawn_async(args, NULL, TR_IF_WIN32("C:\\", "/") "tr-missing-test-work-dir", &error); + check_bool(ret, ==, false); + check_ptr(error, !=, NULL); + check_int(error->code, !=, 0); + check_str(error->message, !=, NULL); + + tr_free(result_path); + libtest_sandbox_destroy(test_dir); + tr_free(test_dir); + return 0; +} + +int main(int argc, char** argv) +{ + self_path = tr_sys_path_resolve(argv[0], NULL); + + if (argc >= 3) + { + char* const result_path = argv[1]; + char* const test_action = argv[2]; + + char* const tmp_result_path = tr_strdup_printf("%s.tmp", result_path); + + tr_sys_file_t const fd = tr_sys_file_open(tmp_result_path, TR_SYS_FILE_WRITE | TR_SYS_FILE_CREATE | + TR_SYS_FILE_TRUNCATE, 0644, NULL); + + if (fd == TR_BAD_SYS_FILE) + { + return 1; + } + + if (strcmp(test_action, arg_dump_args) == 0) + { + for (int i = 3; i < argc; ++i) + { + tr_sys_file_write_line(fd, argv[i], NULL); + } + } + else if (strcmp(test_action, arg_dump_env) == 0) + { + for (int i = 3; i < argc; ++i) + { + tr_sys_file_write_line(fd, tr_env_get_string(argv[i], ""), NULL); + } + } + else if (strcmp(test_action, arg_dump_cwd) == 0) + { + char* const value = tr_sys_dir_get_current(NULL); + tr_sys_file_write_line(fd, value != NULL ? value : "", NULL); + tr_free(value); + } + else + { + tr_sys_file_close(fd, NULL); + tr_sys_path_remove(tmp_result_path, NULL); + return 1; + } + + tr_sys_file_close(fd, NULL); + tr_sys_path_rename(tmp_result_path, result_path, NULL); + return 0; + } + + testFunc const tests[] = + { + test_spawn_async_missing_exe, + test_spawn_async_args, + test_spawn_async_env, + test_spawn_async_cwd_explicit, + test_spawn_async_cwd_inherit, + test_spawn_async_cwd_missing + }; + + int ret = runTests(tests, NUM_TESTS(tests)); + +#ifdef _WIN32 + + strcpy(self_path + strlen(self_path) - 4, ".cmd"); + + int ret2 = runTests(tests, NUM_TESTS(tests)); + + if (ret == 0) + { + ret = ret2; + } + +#endif + + tr_free(self_path); + return ret; +} diff --git a/libtransmission/subprocess-test.cmd b/libtransmission/subprocess-test.cmd new file mode 100644 index 000000000..4d08d062b --- /dev/null +++ b/libtransmission/subprocess-test.cmd @@ -0,0 +1,48 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion + +set __argc=0 +for %%i in (%*) do ( + set /a __argc+=1 + set "__argv[!__argc!]=%%~i" +) + +set "result_path=!__argv[1]!" +set "test_action=!__argv[2]!" + +set "temp_result_path=%result_path%.tmp" +>"%temp_result_path%" >"%temp_result_path%" echo.!__argv[%%i]! + ) + goto finish + +:dump_env + for /l %%i in (3,1,%__argc%) do ( + >>"%temp_result_path%" call :dump_env_var "!__argv[%%i]!" + ) + goto finish + +:dump_env_var + if defined %~1 ( + echo.!%~1! + ) else ( + echo.^ + ) + exit /b 0 + +:dump_cwd + >>"%temp_result_path%" echo.%CD% + goto finish + +:finish + >nul move /y "%temp_result_path%" "%result_path%" + exit /b 0 diff --git a/libtransmission/subprocess-win32.c b/libtransmission/subprocess-win32.c new file mode 100644 index 000000000..67653a63c --- /dev/null +++ b/libtransmission/subprocess-win32.c @@ -0,0 +1,422 @@ +/* + * This file Copyright (C) 2011-2017 Mnemosyne LLC + * + * It may be used under the GNU GPL versions 2 or 3 + * or any future license endorsed by Mnemosyne LLC. + * + */ + +#include +#include +#include +#include + +#include + +#include "transmission.h" +#include "error.h" +#include "subprocess.h" +#include "tr-assert.h" +#include "utils.h" + +enum tr_app_type +{ + TR_APP_TYPE_EXE, + TR_APP_TYPE_BATCH +}; + +static void set_system_error(tr_error** error, DWORD code, char const* what) +{ + if (error == NULL) + { + return; + } + + char* message = tr_win32_format_message(code); + + if (message == NULL) + { + message = tr_strdup_printf("Unknown error: 0x%08lx", code); + } + + if (what == NULL) + { + tr_error_set_literal(error, code, message); + } + else + { + tr_error_set(error, code, "%s failed: %s", what, message); + } + + tr_free(message); +} + +static void append_to_env_block(wchar_t** env_block, size_t* env_block_len, wchar_t const* part, size_t part_len) +{ + *env_block = tr_renew(wchar_t, *env_block, *env_block_len + part_len + 1); + wmemcpy(*env_block + *env_block_len, part, part_len); + *env_block_len += part_len; +} + +static bool parse_env_block_part(wchar_t const* part, size_t* full_len, size_t* name_len) +{ + TR_ASSERT(part != NULL); + + wchar_t* const equals_pos = wcschr(part, L'='); + + if (equals_pos == NULL) + { + /* Invalid part */ + return false; + } + + ptrdiff_t const my_name_len = equals_pos - part; + + if (my_name_len > SIZE_MAX) + { + /* Invalid part */ + return false; + } + + if (full_len != NULL) + { + /* Includes terminating '\0' */ + *full_len = wcslen(part) + 1; + } + + if (name_len != NULL) + { + *name_len = (size_t)my_name_len; + } + + return true; +} + +static int compare_wide_strings_ci(wchar_t const* lhs, size_t lhs_len, wchar_t const* rhs, size_t rhs_len) +{ + int diff = wcsnicmp(lhs, rhs, MIN(lhs_len, rhs_len)); + + if (diff == 0) + { + diff = lhs_len < rhs_len ? -1 : (lhs_len > rhs_len ? 1 : 0); + } + + return diff; +} + +static int compare_env_part_names(wchar_t const** lhs, wchar_t const** rhs) +{ + int ret = 0; + + size_t lhs_part_len; + size_t lhs_name_len; + + if (parse_env_block_part(*lhs, &lhs_part_len, &lhs_name_len)) + { + size_t rhs_part_len; + size_t rhs_name_len; + + if (parse_env_block_part(*rhs, &rhs_part_len, &rhs_name_len)) + { + ret = compare_wide_strings_ci(*lhs, lhs_name_len, *rhs, rhs_name_len); + } + } + + return ret; +} + +static wchar_t** to_wide_env(char const* const* env) +{ + if (env == NULL || env[0] == NULL) + { + return NULL; + } + + size_t part_count = 0; + + while (env[part_count] != NULL) + { + ++part_count; + } + + wchar_t** wide_env = tr_new(wchar_t*, part_count + 1); + + for (size_t i = 0; i < part_count; ++i) + { + wide_env[i] = tr_win32_utf8_to_native(env[i], -1); + } + + wide_env[part_count] = NULL; + + /* "The sort is case-insensitive, Unicode order, without regard to locale" (c) MSDN */ + qsort(wide_env, part_count, sizeof(wchar_t*), &compare_env_part_names); + + return wide_env; +} + +static bool create_env_block(char const* const* env, wchar_t** env_block, tr_error** error) +{ + wchar_t** wide_env = to_wide_env(env); + + if (wide_env == NULL) + { + *env_block = NULL; + return true; + } + + wchar_t* const old_env_block = GetEnvironmentStringsW(); + + if (old_env_block == NULL) + { + set_system_error(error, GetLastError(), "Call to GetEnvironmentStrings()"); + return false; + } + + *env_block = NULL; + + wchar_t const* old_part = old_env_block; + size_t env_block_len = 0; + + for (size_t i = 0; wide_env[i] != NULL; ++i) + { + wchar_t const* const part = wide_env[i]; + + size_t part_len; + size_t name_len; + + if (!parse_env_block_part(part, &part_len, &name_len)) + { + continue; + } + + while (*old_part != L'\0') + { + size_t old_part_len; + size_t old_name_len; + + if (!parse_env_block_part(old_part, &old_part_len, &old_name_len)) + { + continue; + } + + int const name_diff = compare_wide_strings_ci(old_part, old_name_len, part, name_len); + + if (name_diff < 0) + { + append_to_env_block(env_block, &env_block_len, old_part, old_part_len); + } + + if (name_diff <= 0) + { + old_part += old_part_len; + } + + if (name_diff >= 0) + { + break; + } + } + + append_to_env_block(env_block, &env_block_len, part, part_len); + } + + while (*old_part != L'\0') + { + size_t old_part_len; + + if (!parse_env_block_part(old_part, &old_part_len, NULL)) + { + continue; + } + + append_to_env_block(env_block, &env_block_len, old_part, old_part_len); + old_part += old_part_len; + } + + (*env_block)[env_block_len] = '\0'; + + FreeEnvironmentStringsW(old_env_block); + + tr_free_ptrv((void* const*)wide_env); + tr_free(wide_env); + + return true; +} + +static void append_argument(char** arguments, char const* argument) +{ + size_t arguments_len = *arguments != NULL ? strlen(*arguments) : 0u; + size_t const argument_len = strlen(argument); + + if (arguments_len > 0) + { + (*arguments)[arguments_len++] = ' '; + } + + if (argument[0] != '\0' && strpbrk(argument, " \t\n\v\"") == NULL) + { + *arguments = tr_renew(char, *arguments, arguments_len + argument_len + 2); + strcpy(*arguments + arguments_len, argument); + return; + } + + *arguments = tr_renew(char, *arguments, arguments_len + argument_len * 2 + 4); + + char* dst = *arguments + arguments_len; + *(dst++) = '"'; + + for (char const* src = argument; *src != '\0';) + { + size_t backslash_count = 0; + + while (*src == '\\') + { + ++backslash_count; + ++src; + } + + switch (*src) + { + case '\0': + backslash_count = backslash_count * 2; + break; + + case '"': + backslash_count = backslash_count * 2 + 1; + break; + } + + if (backslash_count != 0) + { + memset(dst, '\\', backslash_count); + dst += backslash_count; + } + + if (*src != '\0') + { + *(dst++) = *(src++); + } + } + + *(dst++) = '"'; + *(dst++) = '\0'; +} + +static bool contains_batch_metachars(char const* text) +{ + /* First part - chars explicitly documented by `cmd.exe /?` as "special" */ + return strpbrk(text, "&<>()@^|" "%!^\"") != NULL; +} + +static enum tr_app_type get_app_type(char const* app) +{ + if (tr_str_has_suffix(app, ".cmd") || tr_str_has_suffix(app, ".bat")) + { + return TR_APP_TYPE_BATCH; + } + + /* TODO: Support other types? */ + + return TR_APP_TYPE_EXE; +} + +static void append_app_launcher_arguments(enum tr_app_type app_type, char** args) +{ + switch (app_type) + { + case TR_APP_TYPE_EXE: + break; + + case TR_APP_TYPE_BATCH: + append_argument(args, "cmd.exe"); + append_argument(args, "/d"); + append_argument(args, "/e:off"); + append_argument(args, "/v:off"); + append_argument(args, "/s"); + append_argument(args, "/c"); + break; + + default: + TR_ASSERT_MSG(false, "unsupported application type %d", (int)app_type); + break; + } +} + +static bool construct_cmd_line(char const* const* cmd, wchar_t** cmd_line) +{ + enum tr_app_type const app_type = get_app_type(cmd[0]); + + char* args = NULL; + size_t arg_count = 0; + bool ret = false; + + append_app_launcher_arguments(app_type, &args); + + for (size_t i = 0; cmd[i] != NULL; ++i) + { + if (app_type == TR_APP_TYPE_BATCH && i > 0 && contains_batch_metachars(cmd[i])) + { + /* FIXME: My attempts to escape them one or another way didn't lead to anything good so far */ + goto cleanup; + } + + append_argument(&args, cmd[i]); + ++arg_count; + } + + *cmd_line = args != NULL ? tr_win32_utf8_to_native(args, -1) : NULL; + + ret = true; + +cleanup: + tr_free(args); + return ret; +} + +bool tr_spawn_async(char* const* cmd, char* const* env, char const* work_dir, tr_error** error) +{ + wchar_t* env_block = NULL; + + if (!create_env_block(env, &env_block, error)) + { + return false; + } + + wchar_t* cmd_line; + + if (!construct_cmd_line(cmd, &cmd_line)) + { + set_system_error(error, ERROR_INVALID_PARAMETER, "Constructing command line"); + return false; + } + + wchar_t* current_dir = work_dir != NULL ? tr_win32_utf8_to_native(work_dir, -1) : NULL; + + STARTUPINFOW si = + { + .cb = sizeof(si), + .dwFlags = STARTF_USESHOWWINDOW, + .wShowWindow = SW_HIDE + }; + + PROCESS_INFORMATION pi; + + bool const ret = CreateProcessW(NULL, cmd_line, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT | + CREATE_NO_WINDOW | CREATE_DEFAULT_ERROR_MODE, env_block, current_dir, &si, &pi); + + if (ret) + { + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + } + else + { + set_system_error(error, GetLastError(), "Call to CreateProcess()"); + } + + tr_free(current_dir); + tr_free(cmd_line); + tr_free(env_block); + + return ret; +} diff --git a/libtransmission/subprocess.h b/libtransmission/subprocess.h new file mode 100644 index 000000000..79ac7b4e4 --- /dev/null +++ b/libtransmission/subprocess.h @@ -0,0 +1,11 @@ +/* + * This file Copyright (C) 2017 Mnemosyne LLC + * + * It may be used under the GNU GPL versions 2 or 3 + * or any future license endorsed by Mnemosyne LLC. + * + */ + +#pragma once + +bool tr_spawn_async(char* const* cmd, char* const* env, char const* work_dir, struct tr_error** error); diff --git a/libtransmission/torrent.c b/libtransmission/torrent.c index ee0631a2f..b0f43f773 100644 --- a/libtransmission/torrent.c +++ b/libtransmission/torrent.c @@ -1,5 +1,5 @@ /* - * This file Copyright (C) 2009-2014 Mnemosyne LLC + * This file Copyright (C) 2009-2017 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. @@ -43,6 +43,7 @@ #include "ptrarray.h" #include "resume.h" #include "session.h" +#include "subprocess.h" #include "torrent.h" #include "torrent-magnet.h" #include "tr-assert.h" @@ -2213,149 +2214,63 @@ void tr_torrentClearIdleLimitHitCallback(tr_torrent* torrent) tr_torrentSetIdleLimitHitCallback(torrent, NULL, NULL); } -#ifndef _WIN32 - -static void onSigCHLD(int i UNUSED) +static void get_local_time_str(char* const buffer, size_t const buffer_len) { - int rc; + time_t const now = tr_time(); - do + tr_strlcpy(buffer, ctime(&now), buffer_len); + + char* newline_pos = strchr(buffer, '\n'); + + /* ctime() includes '\n', but it's better to be safe */ + if (newline_pos != NULL) { - rc = waitpid(-1, NULL, WNOHANG); + *newline_pos = '\0'; } - while (rc > 0 || (rc == -1 && errno == EINTR)); } -#endif - static void torrentCallScript(tr_torrent const* tor, char const* script) { - char timeStr[128]; - char* newlinePos; - time_t const now = tr_time(); - - tr_strlcpy(timeStr, ctime(&now), sizeof(timeStr)); - - /* ctime () includes '\n', but it's better to be safe */ - newlinePos = strchr(timeStr, '\n'); - - if (newlinePos != NULL) + if (script == NULL || *script == '\0') { - *newlinePos = '\0'; + return; } - if (script != NULL && *script != '\0') + char time_str[32]; + get_local_time_str(time_str, TR_N_ELEMENTS(time_str)); + + char* const torrent_dir = tr_sys_path_native_separators(tr_strdup(tor->currentDir)); + + char* const cmd[] = { - char* cmd[] = - { - tr_strdup(script), NULL - }; - char* env[] = - { - tr_strdup_printf("TR_APP_VERSION=%s", SHORT_VERSION_STRING), - tr_strdup_printf("TR_TIME_LOCALTIME=%s", timeStr), - tr_strdup_printf("TR_TORRENT_DIR=%s", tor->currentDir), - tr_strdup_printf("TR_TORRENT_HASH=%s", tor->info.hashString), - tr_strdup_printf("TR_TORRENT_ID=%d", tr_torrentId(tor)), - tr_strdup_printf("TR_TORRENT_NAME=%s", tr_torrentName(tor)), - NULL - }; + tr_strdup(script), + NULL + }; - tr_logAddTorInfo(tor, "Calling script \"%s\"", script); + char* const env[] = + { + tr_strdup_printf("TR_APP_VERSION=%s", SHORT_VERSION_STRING), + tr_strdup_printf("TR_TIME_LOCALTIME=%s", time_str), + tr_strdup_printf("TR_TORRENT_DIR=%s", torrent_dir), + tr_strdup_printf("TR_TORRENT_HASH=%s", tor->info.hashString), + tr_strdup_printf("TR_TORRENT_ID=%d", tr_torrentId(tor)), + tr_strdup_printf("TR_TORRENT_NAME=%s", tr_torrentName(tor)), + NULL + }; -#ifdef TR_ENABLE_ASSERTS + tr_logAddTorInfo(tor, "Calling script \"%s\"", script); - /* Win32 environment block strings should be sorted alphabetically */ - for (size_t i = 1; env[i] != NULL; ++i) - { - TR_ASSERT(strcmp(env[i - 1], env[i]) < 0); - } + tr_error* error = NULL; -#endif - -#ifdef _WIN32 - - wchar_t* wide_script = tr_win32_utf8_to_native(script, -1); - - size_t env_block_size = 0; - char* env_block = NULL; - - for (size_t i = 0; env[i] != NULL; ++i) - { - size_t const len = strlen(env[i]) + 1; - env_block = tr_renew(char, env_block, env_block_size + len + 1); - memcpy(env_block + env_block_size, env[i], len + 1); - env_block_size += len; - } - - wchar_t* wide_env_block = NULL; - - if (env_block != NULL) - { - env_block[env_block_size] = '\0'; - wide_env_block = tr_win32_utf8_to_native(env_block, env_block_size + 1); - tr_free(env_block); - } - - STARTUPINFOW si = { 0, }; - si.cb = sizeof(si); - si.dwFlags = STARTF_USESHOWWINDOW; - si.wShowWindow = SW_HIDE; - - PROCESS_INFORMATION pi; - - if (CreateProcessW(wide_script, NULL, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT | - CREATE_NO_WINDOW | CREATE_DEFAULT_ERROR_MODE | DETACHED_PROCESS, wide_env_block, L"\\", &si, &pi)) - { - CloseHandle(pi.hThread); - CloseHandle(pi.hProcess); - } - else - { - char* const message = tr_win32_format_message(GetLastError()); - tr_logAddTorErr(tor, "error executing script \"%s\": %s", script, message); - tr_free(message); - } - - tr_free(wide_env_block); - tr_free(wide_script); - -#else /* _WIN32 */ - - signal(SIGCHLD, onSigCHLD); - - if (fork() == 0) - { - for (size_t i = 0; env[i] != NULL; ++i) - { - putenv(env[i]); - } - - if (chdir("/") == -1) - { - /* ignore (nice to have but not that critical) */ - } - - if (execvp(script, cmd) == -1) - { - tr_logAddTorErr(tor, "error executing script \"%s\": %s", script, tr_strerror(errno)); - } - - _exit(0); - } - -#endif /* _WIN32 */ - - for (size_t i = 0; cmd[i] != NULL; ++i) - { - tr_free(cmd[i]); - } - - for (size_t i = 0; env[i] != NULL; ++i) - { - tr_free(env[i]); - } + if (!tr_spawn_async(cmd, env, TR_IF_WIN32("\\", "/"), &error)) + { + tr_logAddTorErr(tor, "Error executing script \"%s\" (%d): %s", script, error->code, error->message); + tr_error_free(error); } + + tr_free_ptrv((void* const*)env); + tr_free_ptrv((void* const*)cmd); + tr_free(torrent_dir); } void tr_torrentRecheckCompleteness(tr_torrent* tor) diff --git a/libtransmission/tr-macros.h b/libtransmission/tr-macros.h index 44cc0dc07..2e3f740c0 100644 --- a/libtransmission/tr-macros.h +++ b/libtransmission/tr-macros.h @@ -28,6 +28,12 @@ #define __has_builtin(x) 0 #endif +#ifdef _WIN32 +#define TR_IF_WIN32(ThenValue, ElseValue) ThenValue +#else +#define TR_IF_WIN32(ThenValue, ElseValue) ElseValue +#endif + #ifdef __GNUC__ #define TR_GNUC_CHECK_VERSION(major, minor) \ (__GNUC__ > (major) || \ diff --git a/libtransmission/utils.c b/libtransmission/utils.c index bbbda8c1c..ae6e60665 100644 --- a/libtransmission/utils.c +++ b/libtransmission/utils.c @@ -1,5 +1,5 @@ /* - * This file Copyright (C) 2009-2014 Mnemosyne LLC + * This file Copyright (C) 2009-2017 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. @@ -152,6 +152,20 @@ void tr_free(void* p) } } +void tr_free_ptrv(void* const* p) +{ + if (p == NULL) + { + return; + } + + while (*p != NULL) + { + tr_free(*p); + ++p; + } +} + void* tr_memdup(void const* src, size_t byteCount) { return memcpy(tr_malloc(byteCount), src, byteCount); diff --git a/libtransmission/utils.h b/libtransmission/utils.h index 628684264..2b0f98b6c 100644 --- a/libtransmission/utils.h +++ b/libtransmission/utils.h @@ -1,5 +1,5 @@ /* - * This file Copyright (C) 2009-2014 Mnemosyne LLC + * This file Copyright (C) 2009-2017 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. @@ -154,6 +154,9 @@ void* tr_realloc(void* p, size_t size); /** @brief Portability wrapper around free() in which `NULL' is a safe argument */ void tr_free(void* p); +/** @brief Free pointers in a NULL-terminated array (the array itself is not freed) */ +void tr_free_ptrv(void* const* p); + /** * @brief make a newly-allocated copy of a chunk of memory * @param src the memory to copy