1
0
Fork 0
mirror of https://github.com/transmission/transmission synced 2024-12-29 02:56:11 +00:00
transmission/cli/cli.cc
Mike Gelfand e8fcb025a0
fix: use user-preferred locale (#5444)
* Use user-preferred locale

Previous fix adding `L` format specifier was correct but only fixed half
of the problem, as C++ locale is set up to be "C" by default. GTK client
used to call `setlocale(LC_ALL, "")` to set up user-preferred locale
which only affected C functions and `std::locale` (used by libfmt) was
unaware of those changes.

Apply the fix to all the binaries since they're all doing some sort of
output to the user and calling libtransmission helpers, as well as using
libfmt directly.

* Improve libtransmission's json-test

Set the locale C++ way to avoid any sort of inconsistencies, and also
restore it to the old one once finished testing.

* Improve transmission-show test runner script

Quote outputs to avoid CMake error about `message()` being called with
no arguments. Capture stderr to the same output file. Fallback to `git
diff` if `diff` wasn't found. A few other minor changes.
2023-04-27 09:10:33 -05:00

492 lines
14 KiB
C++

// This file Copyright © 2006-2023 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#include <array>
#include <cstdio> /* fprintf () */
#include <cstdlib> /* atoi () */
#include <string>
#include <string_view>
#include <signal.h>
#include <fmt/core.h>
#include <libtransmission/transmission.h>
#include <libtransmission/error.h>
#include <libtransmission/file.h>
#include <libtransmission/tr-getopt.h>
#include <libtransmission/utils.h> /* tr_wait() */
#include <libtransmission/variant.h>
#include <libtransmission/version.h>
#include <libtransmission/web-utils.h>
#include <libtransmission/web.h> // tr_sessionFetch()
using namespace std::chrono_literals;
/***
****
***/
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 };
#define SPEED_K_STR "kB/s"
static char constexpr SpeedKStr[] = SPEED_K_STR;
static char constexpr SpeedMStr[] = "MB/s";
static char constexpr SpeedGStr[] = "GB/s";
static char constexpr SpeedTStr[] = "TB/s";
/***
****
***/
static auto constexpr LineWidth = int{ 80 };
static char constexpr MyConfigName[] = "transmission";
static char constexpr MyReadableName[] = "transmission-cli";
static char constexpr Usage
[] = "A fast and easy BitTorrent client\n"
"\n"
"Usage: transmission-cli [options] <file|url|magnet>";
static bool showVersion = false;
static bool verify = false;
static sig_atomic_t gotsig = false;
static sig_atomic_t manualUpdate = false;
static char const* torrentPath = nullptr;
static auto constexpr Options = std::array<tr_option, 20>{
{ { 'b', "blocklist", "Enable peer blocklists", "b", false, nullptr },
{ 'B', "no-blocklist", "Disable peer blocklists", "B", false, nullptr },
{ 'd', "downlimit", "Set max download speed in " SPEED_K_STR, "d", true, "<speed>" },
{ 'D', "no-downlimit", "Don't limit the download speed", "D", false, nullptr },
{ 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 },
{ 'f', "finish", "Run a script when the torrent finishes", "f", true, "<script>" },
{ 'g', "config-dir", "Where to find configuration files", "g", true, "<path>" },
{ 'm', "portmap", "Enable portmapping via NAT-PMP or UPnP", "m", false, nullptr },
{ 'M', "no-portmap", "Disable portmapping", "M", false, nullptr },
{ 'p', "port", "Port for incoming peers (Default: " TR_DEFAULT_PEER_PORT_STR ")", "p", true, "<port>" },
{ 't',
"tos",
"Peer socket DSCP / ToS setting (number, or a DSCP string, e.g. 'af11' or 'cs0', default=" TR_DEFAULT_PEER_SOCKET_TOS_STR
")",
"t",
true,
"<dscp-or-tos>" },
{ 'u', "uplimit", "Set max upload speed in " SPEED_K_STR, "u", true, "<speed>" },
{ 'U', "no-uplimit", "Don't limit the upload speed", "U", false, nullptr },
{ 'v', "verify", "Verify the specified torrent", "v", false, nullptr },
{ 'V', "version", "Show version number and exit", "V", false, nullptr },
{ 'w', "download-dir", "Where to save downloaded data", "w", true, "<path>" },
{ 500, "sequential-download", "Download pieces sequentially", "seq", false, nullptr },
{ 0, nullptr, nullptr, nullptr, false, nullptr } }
};
static int parseCommandLine(tr_variant*, int argc, char const** argv);
static void sigHandler(int signal);
static std::string tr_strlratio(double ratio)
{
if (static_cast<int>(ratio) == TR_RATIO_NA)
{
return _("None");
}
if (static_cast<int>(ratio) == TR_RATIO_INF)
{
return _("Inf");
}
if (ratio < 10.0)
{
return fmt::format(FMT_STRING("{:.2f}"), ratio);
}
if (ratio < 100.0)
{
return fmt::format(FMT_STRING("{:.1f}"), ratio);
}
return fmt::format(FMT_STRING("{:.0f}"), ratio);
}
static bool waitingOnWeb;
static void onTorrentFileDownloaded(tr_web::FetchResponse const& response)
{
auto* ctor = static_cast<tr_ctor*>(response.user_data);
tr_ctorSetMetainfo(ctor, std::data(response.body), std::size(response.body), nullptr);
waitingOnWeb = false;
}
static std::string getStatusStr(tr_stat const* st)
{
if (st->activity == TR_STATUS_CHECK_WAIT)
{
return "Waiting to verify local files";
}
if (st->activity == TR_STATUS_CHECK)
{
return fmt::format(
FMT_STRING("Verifying local files ({:.2f}%, {:.2f}% valid)"),
tr_truncd(100 * st->recheckProgress, 2),
tr_truncd(100 * st->percentDone, 2));
}
if (st->activity == TR_STATUS_DOWNLOAD)
{
return fmt::format(
FMT_STRING("Progress: {:.1f}%, dl from {:d} of {:d} peers ({:s}), ul to {:d} ({:s}) [{:s}]"),
tr_truncd(100 * st->percentDone, 1),
st->peersSendingToUs,
st->peersConnected,
tr_formatter_speed_KBps(st->pieceDownloadSpeed_KBps),
st->peersGettingFromUs,
tr_formatter_speed_KBps(st->pieceUploadSpeed_KBps),
tr_strlratio(st->ratio));
}
if (st->activity == TR_STATUS_SEED)
{
return fmt::format(
FMT_STRING("Seeding, uploading to {:d} of {:d} peer(s), {:s} [{:s}]"),
st->peersGettingFromUs,
st->peersConnected,
tr_formatter_speed_KBps(st->pieceUploadSpeed_KBps),
tr_strlratio(st->ratio));
}
return "";
}
static std::string getConfigDir(int argc, char const** argv)
{
int c;
char const* my_optarg;
int const ind = tr_optind;
while ((c = tr_getopt(Usage, argc, argv, std::data(Options), &my_optarg)) != TR_OPT_DONE)
{
if (c == 'g')
{
return my_optarg;
break;
}
}
tr_optind = ind;
return tr_getDefaultConfigDir(MyConfigName);
}
int tr_main(int argc, char* argv[])
{
tr_locale_set_global("");
tr_variant settings;
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);
printf("%s %s\n", MyReadableName, LONG_VERSION_STRING);
/* user needs to pass in at least one argument */
if (argc < 2)
{
tr_getopt_usage(MyReadableName, Usage, std::data(Options));
return EXIT_FAILURE;
}
/* load the defaults from config file + libtransmission defaults */
tr_variantInitDict(&settings, 0);
auto const config_dir = getConfigDir(argc, (char const**)argv);
tr_sessionLoadSettings(&settings, config_dir.c_str(), MyConfigName);
/* the command line overrides defaults */
if (parseCommandLine(&settings, argc, (char const**)argv) != 0)
{
return EXIT_FAILURE;
}
if (showVersion)
{
return EXIT_SUCCESS;
}
/* Check the options for validity */
if (torrentPath == nullptr)
{
fprintf(stderr, "No torrent specified!\n");
return EXIT_FAILURE;
}
if (auto sv = std::string_view{}; tr_variantDictFindStrView(&settings, TR_KEY_download_dir, &sv))
{
auto const sz_download_dir = std::string{ sv };
if (!tr_sys_path_exists(sz_download_dir))
{
tr_error* error = nullptr;
if (!tr_sys_dir_create(sz_download_dir, TR_SYS_DIR_CREATE_PARENTS, 0700, &error))
{
fprintf(stderr, "Unable to create download directory \"%s\": %s\n", sz_download_dir.c_str(), error->message);
tr_error_free(error);
return EXIT_FAILURE;
}
}
}
auto* const h = tr_sessionInit(config_dir.c_str(), false, &settings);
auto* const ctor = tr_ctorNew(h);
tr_ctorSetPaused(ctor, TR_FORCE, false);
if (tr_sys_path_exists(torrentPath) ? tr_ctorSetMetainfoFromFile(ctor, torrentPath, nullptr) :
tr_ctorSetMetainfoFromMagnetLink(ctor, torrentPath, nullptr))
{
// all good
}
else if (tr_urlIsValid(torrentPath))
{
// fetch it
tr_sessionFetch(h, { torrentPath, onTorrentFileDownloaded, ctor });
waitingOnWeb = true;
while (waitingOnWeb)
{
tr_wait(1s);
}
}
else
{
fprintf(stderr, "ERROR: Unrecognized torrent \"%s\".\n", torrentPath);
fprintf(stderr, " * If you're trying to create a torrent, use transmission-create.\n");
fprintf(stderr, " * If you're trying to see a torrent's info, use transmission-show.\n");
tr_sessionClose(h);
return EXIT_FAILURE;
}
tr_torrent* tor = tr_torrentNew(ctor, nullptr);
tr_ctorFree(ctor);
if (tor == nullptr)
{
fprintf(stderr, "Failed opening torrent file `%s'\n", torrentPath);
tr_sessionClose(h);
return EXIT_FAILURE;
}
signal(SIGINT, sigHandler);
#ifndef _WIN32
signal(SIGHUP, sigHandler);
#endif
tr_torrentStart(tor);
if (verify)
{
verify = false;
tr_torrentVerify(tor);
}
for (;;)
{
static auto constexpr messageName = std::array<char const*, 4>{
nullptr,
"Tracker gave a warning:",
"Tracker gave an error:",
"Error:",
};
tr_wait(200ms);
if (gotsig)
{
gotsig = false;
printf("\nStopping torrent...\n");
tr_torrentStop(tor);
}
if (manualUpdate)
{
manualUpdate = false;
if (!tr_torrentCanManualUpdate(tor))
{
fprintf(stderr, "\nReceived SIGHUP, but can't send a manual update now\n");
}
else
{
fprintf(stderr, "\nReceived SIGHUP: manual update scheduled\n");
tr_torrentManualUpdate(tor);
}
}
auto const* const st = tr_torrentStat(tor);
if (st->activity == TR_STATUS_STOPPED)
{
break;
}
auto const status_str = getStatusStr(st);
printf("\r%-*s", TR_ARG_TUPLE(LineWidth, status_str.c_str()));
if (messageName[st->error])
{
fprintf(stderr, "\n%s: %s\n", messageName[st->error], st->errorString);
}
}
tr_sessionSaveSettings(h, config_dir.c_str(), &settings);
printf("\n");
tr_variantClear(&settings);
tr_sessionClose(h);
return EXIT_SUCCESS;
}
/***
****
****
***/
static int parseCommandLine(tr_variant* d, int argc, char const** argv)
{
int c;
char const* my_optarg;
while ((c = tr_getopt(Usage, argc, argv, std::data(Options), &my_optarg)) != TR_OPT_DONE)
{
switch (c)
{
case 'b':
tr_variantDictAddBool(d, TR_KEY_blocklist_enabled, true);
break;
case 'B':
tr_variantDictAddBool(d, TR_KEY_blocklist_enabled, false);
break;
case 'd':
tr_variantDictAddInt(d, TR_KEY_speed_limit_down, atoi(my_optarg));
tr_variantDictAddBool(d, TR_KEY_speed_limit_down_enabled, true);
break;
case 'D':
tr_variantDictAddBool(d, TR_KEY_speed_limit_down_enabled, false);
break;
case 'f':
tr_variantDictAddStr(d, TR_KEY_script_torrent_done_filename, my_optarg);
tr_variantDictAddBool(d, TR_KEY_script_torrent_done_enabled, true);
break;
case 'g': /* handled above */
break;
case 'm':
tr_variantDictAddBool(d, TR_KEY_port_forwarding_enabled, true);
break;
case 'M':
tr_variantDictAddBool(d, TR_KEY_port_forwarding_enabled, false);
break;
case 'p':
tr_variantDictAddInt(d, TR_KEY_peer_port, atoi(my_optarg));
break;
case 't':
tr_variantDictAddStr(d, TR_KEY_peer_socket_tos, my_optarg);
break;
case 'u':
tr_variantDictAddInt(d, TR_KEY_speed_limit_up, atoi(my_optarg));
tr_variantDictAddBool(d, TR_KEY_speed_limit_up_enabled, true);
break;
case 'U':
tr_variantDictAddBool(d, TR_KEY_speed_limit_up_enabled, false);
break;
case 'v':
verify = true;
break;
case 'V':
showVersion = true;
break;
case 'w':
tr_variantDictAddStr(d, TR_KEY_download_dir, my_optarg);
break;
case 910:
tr_variantDictAddInt(d, TR_KEY_encryption, TR_ENCRYPTION_REQUIRED);
break;
case 911:
tr_variantDictAddInt(d, TR_KEY_encryption, TR_ENCRYPTION_PREFERRED);
break;
case 912:
tr_variantDictAddInt(d, TR_KEY_encryption, TR_CLEAR_PREFERRED);
break;
case 500:
tr_variantDictAddBool(d, TR_KEY_sequentialDownload, true);
break;
case TR_OPT_UNK:
if (torrentPath == nullptr)
{
torrentPath = my_optarg;
}
break;
default:
return 1;
}
}
return 0;
}
static void sigHandler(int signal)
{
switch (signal)
{
case SIGINT:
gotsig = true;
break;
#ifndef _WIN32
case SIGHUP:
manualUpdate = true;
break;
#endif
default:
break;
}
}