// This file Copyright © 2008-2022 Mnemosyne LLC. // It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), // or any future license endorsed by Mnemosyne LLC. // License text can be found in the licenses/ folder. #include #include #include /* printf */ #include /* atoi */ #include #include #include #include #ifdef HAVE_SYSLOG #include #endif #ifdef _WIN32 #include /* getpid */ #else #include /* getpid */ #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "daemon.h" #ifdef USE_SYSTEMD #include #else static void sd_notify(int /*status*/, char const* /*str*/) { // no-op } static void sd_notifyf(int /*status*/, char const* /*fmt*/, ...) { // no-op } #endif using namespace std::literals; using libtransmission::Watchdir; class Daemon { public: struct event_base* ev_base_ = nullptr; tr_sys_file_t logfile_ = TR_BAD_SYS_FILE; private: bool seen_hup_ = false; bool paused_ = false; std::string config_dir_; char const* log_file_name_ = nullptr; tr_session* my_session_ = nullptr; tr_variant settings_ = {}; tr_quark key_pidfile_ = tr_quark_new("pidfile"sv); tr_quark key_watch_dir_force_generic_ = tr_quark_new("watch-dir-force-generic"sv); public: Daemon() = default; ~Daemon() { tr_variantClear(&settings_); } bool init(int argc, char* argv[], bool* foreground, int* ret); bool parseArgs(int argc, char const** argv, bool* dump_settings, bool* foreground, int* exit_code); bool reopenLogFile(char const* filename); int start(bool (*setupsigfn)(void*), bool foreground); void periodicUpdate(); void reportStatus(); void reconfigure(); void stop(); }; /*** **** ***/ static char constexpr MyName[] = "transmission-daemon"; static char constexpr Usage[] = "Transmission " LONG_VERSION_STRING " https://transmissionbt.com/\n" "A fast and easy BitTorrent client\n" "\n" "transmission-daemon is a headless Transmission session that can be\n" "controlled via transmission-qt, transmission-remote, or its web interface.\n" "\n" "Usage: transmission-daemon [options]"; static auto constexpr MemK = size_t{ 1024 }; static char constexpr MemKStr[] = "KiB"; static char constexpr MemMStr[] = "MiB"; static char constexpr MemGStr[] = "GiB"; static char constexpr MemTStr[] = "TiB"; static auto constexpr DiskK = size_t{ 1000 }; static char constexpr DiskKStr[] = "kB"; static char constexpr DiskMStr[] = "MB"; static char constexpr DiskGStr[] = "GB"; static char constexpr DiskTStr[] = "TB"; static auto constexpr SpeedK = size_t{ 1000 }; static char constexpr SpeedKStr[] = "kB/s"; static char constexpr SpeedMStr[] = "MB/s"; static char constexpr SpeedGStr[] = "GB/s"; static char constexpr SpeedTStr[] = "TB/s"; /*** **** Config File ***/ static auto constexpr Options = std::array{ { { 'a', "allowed", "Allowed IP addresses. (Default: " TR_DEFAULT_RPC_WHITELIST ")", "a", true, "" }, { 'b', "blocklist", "Enable peer blocklists", "b", false, nullptr }, { 'B', "no-blocklist", "Disable peer blocklists", "B", false, nullptr }, { 'c', "watch-dir", "Where to watch for new torrent files", "c", true, "" }, { 'C', "no-watch-dir", "Disable the watch-dir", "C", false, nullptr }, { 941, "incomplete-dir", "Where to store new torrents until they're complete", nullptr, true, "" }, { 942, "no-incomplete-dir", "Don't store incomplete torrents in a different location", nullptr, false, nullptr }, { 'd', "dump-settings", "Dump the settings and exit", "d", false, nullptr }, { 943, "default-trackers", "Trackers for public torrents to use automatically", nullptr, true, "" }, { 'e', "logfile", "Dump the log messages to this filename", "e", true, "" }, { 'f', "foreground", "Run in the foreground instead of daemonizing", "f", false, nullptr }, { 'g', "config-dir", "Where to look for configuration files", "g", true, "" }, { 'p', "port", "RPC port (Default: " TR_DEFAULT_RPC_PORT_STR ")", "p", true, "" }, { 't', "auth", "Require authentication", "t", false, nullptr }, { 'T', "no-auth", "Don't require authentication", "T", false, nullptr }, { 'u', "username", "Set username for authentication", "u", true, "" }, { 'v', "password", "Set password for authentication", "v", true, "" }, { 'V', "version", "Show version number and exit", "V", false, nullptr }, { 810, "log-level", "Must be 'critical', 'error', 'warn', 'info', 'debug', or 'trace'.", nullptr, true, "" }, { 811, "log-error", "Deprecated. Use --log-level=error", nullptr, false, nullptr }, { 812, "log-info", "Deprecated. Use --log-level=info", nullptr, false, nullptr }, { 813, "log-debug", "Deprecated. Use --log-level=debug", nullptr, false, nullptr }, { 'w', "download-dir", "Where to save downloaded data", "w", true, "" }, { 800, "paused", "Pause all torrents on startup", nullptr, false, nullptr }, { 'o', "dht", "Enable distributed hash tables (DHT)", "o", false, nullptr }, { 'O', "no-dht", "Disable distributed hash tables (DHT)", "O", false, nullptr }, { 'y', "lpd", "Enable local peer discovery (LPD)", "y", false, nullptr }, { 'Y', "no-lpd", "Disable local peer discovery (LPD)", "Y", false, nullptr }, { 830, "utp", "Enable uTP for peer connections", nullptr, false, nullptr }, { 831, "no-utp", "Disable uTP for peer connections", nullptr, false, nullptr }, { 'P', "peerport", "Port for incoming peers (Default: " TR_DEFAULT_PEER_PORT_STR ")", "P", true, "" }, { 'm', "portmap", "Enable portmapping via NAT-PMP or UPnP", "m", false, nullptr }, { 'M', "no-portmap", "Disable portmapping", "M", false, nullptr }, { 'L', "peerlimit-global", "Maximum overall number of peers (Default: " TR_DEFAULT_PEER_LIMIT_GLOBAL_STR ")", "L", true, "" }, { 'l', "peerlimit-torrent", "Maximum number of peers per torrent (Default: " TR_DEFAULT_PEER_LIMIT_TORRENT_STR ")", "l", true, "" }, { 910, "encryption-required", "Encrypt all peer connections", "er", false, nullptr }, { 911, "encryption-preferred", "Prefer encrypted peer connections", "ep", false, nullptr }, { 912, "encryption-tolerated", "Prefer unencrypted peer connections", "et", false, nullptr }, { 'i', "bind-address-ipv4", "Where to listen for peer connections", "i", true, "" }, { 'I', "bind-address-ipv6", "Where to listen for peer connections", "I", true, "" }, { 'r', "rpc-bind-address", "Where to listen for RPC connections", "r", true, "" }, { 953, "global-seedratio", "All torrents, unless overridden by a per-torrent setting, should seed until a specific ratio", "gsr", true, "ratio" }, { 954, "no-global-seedratio", "All torrents, unless overridden by a per-torrent setting, should seed regardless of ratio", "GSR", false, nullptr }, { 'x', "pid-file", "Enable PID file", "x", true, "" }, { 0, nullptr, nullptr, nullptr, false, nullptr } } }; bool Daemon::reopenLogFile(char const* filename) { tr_error* error = nullptr; tr_sys_file_t const old_log_file = logfile_; tr_sys_file_t const new_log_file = tr_sys_file_open( filename, TR_SYS_FILE_WRITE | TR_SYS_FILE_CREATE | TR_SYS_FILE_APPEND, 0666, &error); if (new_log_file == TR_BAD_SYS_FILE) { fprintf(stderr, "Couldn't (re)open log file \"%s\": %s\n", filename, error->message); tr_error_free(error); return false; } logfile_ = new_log_file; if (old_log_file != TR_BAD_SYS_FILE) { tr_sys_file_close(old_log_file); } return true; } static std::string getConfigDir(int argc, char const* const* argv) { int c; char const* optstr; int const ind = tr_optind; while ((c = tr_getopt(Usage, argc, argv, std::data(Options), &optstr)) != TR_OPT_DONE) { if (c == 'g') { return optstr; } } tr_optind = ind; return tr_getDefaultConfigDir(MyName); } static auto onFileAdded(tr_session* session, std::string_view dirname, std::string_view basename) { auto const lowercase = tr_strlower(basename); auto const is_torrent = tr_strvEndsWith(lowercase, ".torrent"sv); auto const is_magnet = tr_strvEndsWith(lowercase, ".magnet"sv); if (!is_torrent && !is_magnet) { return Watchdir::Action::Done; } auto const filename = tr_pathbuf{ dirname, '/', basename }; tr_ctor* const ctor = tr_ctorNew(session); bool retry = false; if (is_torrent) { if (!tr_ctorSetMetainfoFromFile(ctor, filename, nullptr)) { retry = true; } } else // is_magnet { auto content = std::vector{}; tr_error* error = nullptr; if (!tr_loadFile(filename, content, &error)) { tr_logAddWarn(fmt::format( _("Couldn't read '{path}': {error} ({error_code})"), fmt::arg("path", basename), fmt::arg("error", error->message), fmt::arg("error_code", error->code))); tr_error_free(error); retry = true; } else { content.push_back('\0'); // zero-terminated string auto const* data = std::data(content); if (!tr_ctorSetMetainfoFromMagnetLink(ctor, data, nullptr)) { retry = true; } } } if (retry) { tr_ctorFree(ctor); return Watchdir::Action::Retry; } if (tr_torrentNew(ctor, nullptr) == nullptr) { tr_logAddError(fmt::format(_("Couldn't add torrent file '{path}'"), fmt::arg("path", basename))); } else { bool trash = false; bool const test = tr_ctorGetDeleteSource(ctor, &trash); if (test && trash) { tr_error* error = nullptr; tr_logAddInfo(fmt::format(_("Removing torrent file '{path}'"), fmt::arg("path", basename))); if (!tr_sys_path_remove(filename, &error)) { tr_logAddError(fmt::format( _("Couldn't remove '{path}': {error} ({error_code})"), fmt::arg("path", basename), fmt::arg("error", error->message), fmt::arg("error_code", error->code))); tr_error_free(error); } } else { tr_sys_path_rename(filename, tr_pathbuf{ filename, ".added"sv }); } } tr_ctorFree(ctor); return Watchdir::Action::Done; } static char const* levelName(tr_log_level level) { switch (level) { case TR_LOG_CRITICAL: return "CRT"; case TR_LOG_ERROR: return "ERR"; case TR_LOG_WARN: return "WRN"; case TR_LOG_DEBUG: return "dbg"; case TR_LOG_TRACE: return "trc"; default: return "inf"; } } static void printMessage( tr_sys_file_t file, tr_log_level level, std::string_view name, std::string_view message, std::string_view filename, int line) { auto const out = std::empty(name) ? fmt::format(FMT_STRING("{:s} ({:s}:{:d})"), message, filename, line) : fmt::format(FMT_STRING("{:s} {:s} ({:s}:{:d})"), name, message, filename, line); if (file != TR_BAD_SYS_FILE) { auto timestr = std::array{}; tr_logGetTimeStr(std::data(timestr), std::size(timestr)); tr_sys_file_write_line(file, fmt::format(FMT_STRING("[{:s}] {:s} {:s}"), std::data(timestr), levelName(level), out)); } #ifdef HAVE_SYSLOG else /* daemon... write to syslog */ { int priority; /* figure out the syslog priority */ switch (level) { case TR_LOG_CRITICAL: priority = LOG_CRIT; break; case TR_LOG_ERROR: priority = LOG_ERR; break; case TR_LOG_WARN: priority = LOG_WARNING; break; case TR_LOG_INFO: priority = LOG_INFO; break; default: priority = LOG_DEBUG; break; } syslog(priority, "%s", out.c_str()); } #endif } static void pumpLogMessages(tr_sys_file_t file) { tr_log_message* list = tr_logGetQueue(); for (tr_log_message const* l = list; l != nullptr; l = l->next) { printMessage(file, l->level, l->name, l->message, l->file, l->line); } if (file != TR_BAD_SYS_FILE) { tr_sys_file_flush(file); } tr_logFreeQueue(list); } void Daemon::reportStatus(void) { double const up = tr_sessionGetRawSpeed_KBps(my_session_, TR_UP); double const dn = tr_sessionGetRawSpeed_KBps(my_session_, TR_DOWN); if (up > 0 || dn > 0) { sd_notifyf(0, "STATUS=Uploading %.2f KBps, Downloading %.2f KBps.\n", up, dn); } else { sd_notify(0, "STATUS=Idle.\n"); } } void Daemon::periodicUpdate(void) { pumpLogMessages(logfile_); reportStatus(); } static void periodicUpdate(evutil_socket_t /*fd*/, short /*what*/, void* arg) { static_cast(arg)->periodicUpdate(); } static tr_rpc_callback_status on_rpc_callback( tr_session* /*session*/, tr_rpc_callback_type type, tr_torrent* /*tor*/, void* arg) { Daemon* daemon = static_cast(arg); if (type == TR_RPC_SESSION_CLOSE) { event_base_loopexit(daemon->ev_base_, nullptr); } return TR_RPC_OK; } bool Daemon::parseArgs(int argc, char const** argv, bool* dump_settings, bool* foreground, int* exit_code) { int c; char const* optstr; paused_ = false; *dump_settings = false; *foreground = false; tr_optind = 1; while ((c = tr_getopt(Usage, argc, argv, std::data(Options), &optstr)) != TR_OPT_DONE) { switch (c) { case 'a': tr_variantDictAddStr(&settings_, TR_KEY_rpc_whitelist, optstr); tr_variantDictAddBool(&settings_, TR_KEY_rpc_whitelist_enabled, true); break; case 'b': tr_variantDictAddBool(&settings_, TR_KEY_blocklist_enabled, true); break; case 'B': tr_variantDictAddBool(&settings_, TR_KEY_blocklist_enabled, false); break; case 'c': tr_variantDictAddStr(&settings_, TR_KEY_watch_dir, optstr); tr_variantDictAddBool(&settings_, TR_KEY_watch_dir_enabled, true); break; case 'C': tr_variantDictAddBool(&settings_, TR_KEY_watch_dir_enabled, false); break; case 941: tr_variantDictAddStr(&settings_, TR_KEY_incomplete_dir, optstr); tr_variantDictAddBool(&settings_, TR_KEY_incomplete_dir_enabled, true); break; case 942: tr_variantDictAddBool(&settings_, TR_KEY_incomplete_dir_enabled, false); break; case 943: tr_variantDictAddStr(&settings_, TR_KEY_default_trackers, optstr); break; case 'd': *dump_settings = true; break; case 'e': if (reopenLogFile(optstr)) { log_file_name_ = optstr; } break; case 'f': *foreground = true; break; case 'g': /* handled above */ break; case 'V': /* version */ fprintf(stderr, "%s %s\n", MyName, LONG_VERSION_STRING); *exit_code = 0; return false; case 'o': tr_variantDictAddBool(&settings_, TR_KEY_dht_enabled, true); break; case 'O': tr_variantDictAddBool(&settings_, TR_KEY_dht_enabled, false); break; case 'p': tr_variantDictAddInt(&settings_, TR_KEY_rpc_port, atoi(optstr)); break; case 't': tr_variantDictAddBool(&settings_, TR_KEY_rpc_authentication_required, true); break; case 'T': tr_variantDictAddBool(&settings_, TR_KEY_rpc_authentication_required, false); break; case 'u': tr_variantDictAddStr(&settings_, TR_KEY_rpc_username, optstr); break; case 'v': tr_variantDictAddStr(&settings_, TR_KEY_rpc_password, optstr); break; case 'w': tr_variantDictAddStr(&settings_, TR_KEY_download_dir, optstr); break; case 'P': tr_variantDictAddInt(&settings_, TR_KEY_peer_port, atoi(optstr)); break; case 'm': tr_variantDictAddBool(&settings_, TR_KEY_port_forwarding_enabled, true); break; case 'M': tr_variantDictAddBool(&settings_, TR_KEY_port_forwarding_enabled, false); break; case 'L': tr_variantDictAddInt(&settings_, TR_KEY_peer_limit_global, atoi(optstr)); break; case 'l': tr_variantDictAddInt(&settings_, TR_KEY_peer_limit_per_torrent, atoi(optstr)); break; case 800: paused_ = true; break; case 910: tr_variantDictAddInt(&settings_, TR_KEY_encryption, TR_ENCRYPTION_REQUIRED); break; case 911: tr_variantDictAddInt(&settings_, TR_KEY_encryption, TR_ENCRYPTION_PREFERRED); break; case 912: tr_variantDictAddInt(&settings_, TR_KEY_encryption, TR_CLEAR_PREFERRED); break; case 'i': tr_variantDictAddStr(&settings_, TR_KEY_bind_address_ipv4, optstr); break; case 'I': tr_variantDictAddStr(&settings_, TR_KEY_bind_address_ipv6, optstr); break; case 'r': tr_variantDictAddStr(&settings_, TR_KEY_rpc_bind_address, optstr); break; case 953: tr_variantDictAddReal(&settings_, TR_KEY_ratio_limit, atof(optstr)); tr_variantDictAddBool(&settings_, TR_KEY_ratio_limit_enabled, true); break; case 954: tr_variantDictAddBool(&settings_, TR_KEY_ratio_limit_enabled, false); break; case 'x': tr_variantDictAddStr(&settings_, key_pidfile_, optstr); break; case 'y': tr_variantDictAddBool(&settings_, TR_KEY_lpd_enabled, true); break; case 'Y': tr_variantDictAddBool(&settings_, TR_KEY_lpd_enabled, false); break; case 810: if (auto const level = tr_logGetLevelFromKey(optstr); level) { tr_variantDictAddInt(&settings_, TR_KEY_message_level, *level); } else { std::cerr << fmt::format(_("Couldn't parse log level '{level}'"), fmt::arg("level", optstr)) << std::endl; } break; case 811: std::cerr << "WARN: --log-error is deprecated. Use --log-level=error" << std::endl; tr_variantDictAddInt(&settings_, TR_KEY_message_level, TR_LOG_ERROR); break; case 812: std::cerr << "WARN: --log-info is deprecated. Use --log-level=info" << std::endl; tr_variantDictAddInt(&settings_, TR_KEY_message_level, TR_LOG_INFO); break; case 813: std::cerr << "WARN: --log-debug is deprecated. Use --log-level=debug" << std::endl; tr_variantDictAddInt(&settings_, TR_KEY_message_level, TR_LOG_DEBUG); break; case 830: tr_variantDictAddBool(&settings_, TR_KEY_utp_enabled, true); break; case 831: tr_variantDictAddBool(&settings_, TR_KEY_utp_enabled, false); break; default: tr_getopt_usage(MyName, Usage, std::data(Options)); *exit_code = 0; return false; } } return true; } static void daemon_reconfigure(void* arg) { static_cast(arg)->reconfigure(); } void Daemon::reconfigure(void) { if (my_session_ == nullptr) { tr_logAddInfo(_("Deferring reload until session is fully started.")); seen_hup_ = true; } else { tr_variant newsettings; char const* configDir; /* reopen the logfile to allow for log rotation */ if (log_file_name_ != nullptr) { reopenLogFile(log_file_name_); } configDir = tr_sessionGetConfigDir(my_session_); tr_logAddInfo(fmt::format(_("Reloading settings from '{path}'"), fmt::arg("path", configDir))); tr_variantInitDict(&newsettings, 0); tr_variantDictAddBool(&newsettings, TR_KEY_rpc_enabled, true); tr_sessionLoadSettings(&newsettings, configDir, MyName); tr_sessionSet(my_session_, &newsettings); tr_variantClear(&newsettings); tr_sessionReloadBlocklists(my_session_); } } static void daemon_stop(void* arg) { static_cast(arg)->stop(); } void Daemon::stop(void) { event_base_loopexit(ev_base_, nullptr); } int Daemon::start(bool (*setupsigfn)(void*), bool foreground) { bool boolVal; bool pidfile_created = false; tr_session* session = nullptr; struct event* status_ev = nullptr; auto watchdir = std::unique_ptr{}; char const* const cdir = this->config_dir_.c_str(); sd_notifyf(0, "MAINPID=%d\n", (int)getpid()); /* should go before libevent calls */ tr_net_init(); /* setup event state */ ev_base_ = event_base_new(); if (ev_base_ == nullptr || (setupsigfn ? setupsigfn(ev_base_) : true) == false) { auto const error_code = errno; auto const errmsg = fmt::format( _("Couldn't initialize daemon: {error} ({error_code})"), fmt::arg("error", tr_strerror(error_code)), fmt::arg("error_code", error_code)); printMessage(logfile_, TR_LOG_ERROR, MyName, errmsg, __FILE__, __LINE__); return 1; } /* start the session */ tr_formatter_mem_init(MemK, MemKStr, MemMStr, MemGStr, MemTStr); tr_formatter_size_init(DiskK, DiskKStr, DiskMStr, DiskGStr, DiskTStr); tr_formatter_speed_init(SpeedK, SpeedKStr, SpeedMStr, SpeedGStr, SpeedTStr); session = tr_sessionInit(cdir, true, &settings_); tr_sessionSetRPCCallback(session, on_rpc_callback, this); tr_logAddInfo(fmt::format(_("Loading settings from '{path}'"), fmt::arg("path", cdir))); tr_sessionSaveSettings(session, cdir, &settings_); auto sv = std::string_view{}; (void)tr_variantDictFindStrView(&settings_, key_pidfile_, &sv); auto const sz_pid_filename = std::string{ sv }; if (!std::empty(sz_pid_filename)) { tr_error* error = nullptr; tr_sys_file_t fp = tr_sys_file_open( sz_pid_filename.c_str(), TR_SYS_FILE_WRITE | TR_SYS_FILE_CREATE | TR_SYS_FILE_TRUNCATE, 0666, &error); if (fp != TR_BAD_SYS_FILE) { auto const out = std::to_string(getpid()); tr_sys_file_write(fp, std::data(out), std::size(out), nullptr); tr_sys_file_close(fp); tr_logAddInfo(fmt::format(_("Saved pidfile '{path}'"), fmt::arg("path", sz_pid_filename))); pidfile_created = true; } else { tr_logAddError(fmt::format( _("Couldn't save '{path}': {error} ({error_code})"), fmt::arg("path", sz_pid_filename), fmt::arg("error", error->message), fmt::arg("error_code", error->code))); tr_error_free(error); } } if (tr_variantDictFindBool(&settings_, TR_KEY_rpc_authentication_required, &boolVal) && boolVal) { tr_logAddInfo(_("Requiring authentication")); } my_session_ = session; /* If we got a SIGHUP during startup, process that now. */ if (seen_hup_) { daemon_reconfigure(this); } /* maybe add a watchdir */ if (tr_variantDictFindBool(&settings_, TR_KEY_watch_dir_enabled, &boolVal) && boolVal) { auto force_generic = bool{ false }; (void)tr_variantDictFindBool(&settings_, key_watch_dir_force_generic_, &force_generic); auto dir = std::string_view{}; (void)tr_variantDictFindStrView(&settings_, TR_KEY_watch_dir, &dir); if (!std::empty(dir)) { tr_logAddInfo(fmt::format(_("Watching '{path}' for new torrent files"), fmt::arg("path", dir))); auto handler = [session](std::string_view dirname, std::string_view basename) { return onFileAdded(session, dirname, basename); }; auto timer_maker = libtransmission::EvTimerMaker{ ev_base_ }; watchdir = force_generic ? Watchdir::createGeneric(dir, handler, timer_maker) : Watchdir::create(dir, handler, timer_maker, ev_base_); } } /* load the torrents */ { tr_ctor* ctor = tr_ctorNew(my_session_); if (paused_) { tr_ctorSetPaused(ctor, TR_FORCE, true); } tr_sessionLoadTorrents(my_session_, ctor); tr_ctorFree(ctor); } #ifdef HAVE_SYSLOG if (!foreground) { openlog(MyName, LOG_CONS | LOG_PID, LOG_DAEMON); } #endif /* Create new timer event to report daemon status */ { constexpr auto one_sec = timeval{ 1, 0 }; // 1 second status_ev = event_new(ev_base_, -1, EV_PERSIST, &::periodicUpdate, this); if (status_ev == nullptr) { auto const error_code = errno; tr_logAddError(fmt::format( _("Couldn't create event: {error} ({error_code})"), fmt::arg("error", tr_strerror(error_code)), fmt::arg("error_code", error_code))); goto CLEANUP; } if (event_add(status_ev, &one_sec) == -1) { auto const error_code = errno; tr_logAddError(fmt::format( _("Couldn't add event: {error} ({error_code})"), fmt::arg("error", tr_strerror(error_code)), fmt::arg("error_code", error_code))); goto CLEANUP; } } sd_notify(0, "READY=1\n"); /* Run daemon event loop */ if (event_base_dispatch(ev_base_) == -1) { auto const error_code = errno; tr_logAddError(fmt::format( _("Couldn't launch daemon event loop: {error} ({error_code})"), fmt::arg("error", tr_strerror(error_code)), fmt::arg("error_code", error_code))); goto CLEANUP; } CLEANUP: sd_notify(0, "STATUS=Closing transmission session...\n"); printf("Closing transmission session..."); watchdir.reset(); if (status_ev != nullptr) { event_del(status_ev); event_free(status_ev); } event_base_free(ev_base_); tr_sessionSaveSettings(my_session_, cdir, &settings_); tr_sessionClose(my_session_); pumpLogMessages(logfile_); printf(" done.\n"); /* shutdown */ #ifdef HAVE_SYSLOG if (!foreground) { syslog(LOG_INFO, "%s", "Closing session"); closelog(); } #endif /* cleanup */ if (pidfile_created) { tr_sys_path_remove(sz_pid_filename); } sd_notify(0, "STATUS=\n"); return 0; } static int daemon_start(void* varg, bool (*setupsigfn)(void*), bool foreground) { return static_cast(varg)->start(setupsigfn, foreground); } bool Daemon::init(int argc, char* argv[], bool* foreground, int* ret) { config_dir_ = getConfigDir(argc, (char const* const*)argv); /* load settings from defaults + config file */ tr_variantInitDict(&settings_, 0); tr_variantDictAddBool(&settings_, TR_KEY_rpc_enabled, true); bool const loaded = tr_sessionLoadSettings(&settings_, config_dir_.c_str(), MyName); bool dumpSettings; *ret = 0; /* overwrite settings from the command line */ if (!parseArgs(argc, (char const**)argv, &dumpSettings, foreground, ret)) { goto EXIT_EARLY; } if (*foreground && logfile_ == TR_BAD_SYS_FILE) { logfile_ = tr_sys_file_get_std(TR_STD_SYS_FILE_ERR); } if (!loaded) { printMessage(logfile_, TR_LOG_ERROR, MyName, "Error loading config file -- exiting.", __FILE__, __LINE__); *ret = 1; goto EXIT_EARLY; } if (dumpSettings) { auto const str = tr_variantToStr(&settings_, TR_VARIANT_FMT_JSON); fprintf(stderr, "%s", str.c_str()); goto EXIT_EARLY; } return true; EXIT_EARLY: tr_variantClear(&settings_); return false; } int tr_main(int argc, char* argv[]) { Daemon daemon; bool foreground; int ret; if (!daemon.init(argc, argv, &foreground, &ret)) { return ret; } auto constexpr cb = dtr_callbacks{ &daemon_start, &daemon_stop, &daemon_reconfigure, }; if (tr_error* error = nullptr; !dtr_daemon(&cb, &daemon, foreground, &ret, &error)) { auto const errmsg = fmt::format(FMT_STRING("Couldn't daemonize: {:s} ({:d})"), error->message, error->code); printMessage(daemon.logfile_, TR_LOG_ERROR, MyName, errmsg, __FILE__, __LINE__); tr_error_free(error); } return ret; }