diff --git a/docs/COMMAND_LINE.MD b/docs/COMMAND_LINE.MD index 1d4ff65..77e40e5 100644 --- a/docs/COMMAND_LINE.MD +++ b/docs/COMMAND_LINE.MD @@ -44,6 +44,7 @@ --full-validation Enables full share validation / increases CPU usage --onion-address Tell other peers to use this .onion address to connect to this node through TOR --no-clearnet-p2p Forces P2P server to listen on 127.0.0.1 and to not connect to clearnet IPs +--params-file File name to load parameters from. It can't be used together with any other command line parameters ``` ### Example command line @@ -52,6 +53,22 @@ p2pool.exe --host 127.0.0.1 --rpc-port 18081 --zmq-port 18083 --wallet YOUR_WALLET_ADDRESS --stratum 0.0.0.0:3333 --p2p 0.0.0.0:37889 ``` +### Example command line (params file) + +``` +p2pool.exe --params-file params.conf +``` + +params.conf +``` +# IP of your node +host = 127.0.0.1 +wallet = YOUR_WALLET_ADDRESS +data-api = "/path/to/api/folder" # use quotes if you have spaces in the path +loglevel = 4 +mini = 1 +``` + ### Example command line (mining to a subaddress) ``` diff --git a/src/main.cpp b/src/main.cpp index f3960dc..5bf48e4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -108,6 +108,7 @@ void p2pool_usage() "--full-validation Enables full share validation / increases CPU usage\n" "--onion-address Tell other peers to use this .onion address to connect to this node through TOR\n" "--no-clearnet-p2p Forces P2P server to listen on 127.0.0.1 and to not connect to clearnet IPs\n" + "--params-file File name to load parameters from. It can't be used together with any other command line parameters\n" "--help Show this help message\n\n" "Example command line:\n\n" "%s --host 127.0.0.1 --rpc-port 18081 --zmq-port 18083 --wallet YOUR_WALLET_ADDRESS --stratum 0.0.0.0:%d --p2p 0.0.0.0:%d\n\n", @@ -222,10 +223,10 @@ int p2pool_test() return 0; } -static p2pool::Params get_params(int argc, char* argv[]) noexcept +static p2pool::Params get_params(int argc, const char* const argv[]) noexcept { try { - std::vector> args; + std::vector> args; args.reserve(argc); // Group command-line parameters by the pattern "--name [data1 data2 ... data_n]" @@ -236,7 +237,7 @@ static p2pool::Params get_params(int argc, char* argv[]) noexcept if ((arg.size() > 2) && (arg[0] == '-') && (arg[1] == '-')) { // Store the parameter name without the "--" prefix arg.remove_prefix(2); - args.emplace_back(std::vector(1, std::move(arg))); + args.emplace_back(std::vector(1, std::string(arg))); } else if (!args.empty()) { args.back().emplace_back(std::move(arg)); @@ -256,6 +257,22 @@ static p2pool::Params get_params(int argc, char* argv[]) noexcept abort(); } +static p2pool::Params get_params(const std::string& params_file) noexcept +{ + try { + p2pool::Params params(p2pool::parse_config(params_file)); + + if (params.valid()) { + return params; + } + } + catch (const std::exception&) { + } + + printf("Invalid or missing command line. Try \"p2pool --help\".\n"); + abort(); +} + int main(int argc, char* argv[]) { if (argc == 1) { @@ -263,6 +280,8 @@ int main(int argc, char* argv[]) return 0; } + std::string params_file; + for (int i = 1; i < argc; ++i) { if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "/help") || !strcmp(argv[i], "-h") || !strcmp(argv[i], "/h") || !strcmp(argv[i], "/?")) { p2pool_usage(); @@ -277,6 +296,15 @@ int main(int argc, char* argv[]) if (!strcmp(argv[i], "--test")) { return p2pool_test(); } + + if (!strcmp(argv[i], "--params-file") && (i + 1 < argc)) { + params_file = argv[++i]; + } + } + + if (!params_file.empty() && (argc != 3)) { + fprintf(stderr, "--params-file can't be combined with other command line parameters\n"); + return 1; } #if defined(_WIN32) && defined(_MSC_VER) && !defined(NDEBUG) @@ -292,7 +320,7 @@ int main(int argc, char* argv[]) // Some P2Pool code will not work without libuv initialized, so the code above this line must be minimal uv_default_loop(); - const p2pool::Params params = get_params(argc, argv); + const p2pool::Params params = params_file.empty() ? get_params(argc, argv) : get_params(params_file); if (!params.m_dataDir.empty()) { printf("Using \"%s\" for P2Pool files\n", params.m_dataDir.c_str()); diff --git a/src/params.cpp b/src/params.cpp index 9279e46..3695943 100644 --- a/src/params.cpp +++ b/src/params.cpp @@ -30,7 +30,7 @@ namespace p2pool { static constexpr uint64_t MIN_STRATUM_BAN_TIME = UINT64_C(1); static constexpr uint64_t MAX_STRATUM_BAN_TIME = (UINT64_C(1) << 34) - 1; -Params::Params(const std::vector>& args) +Params::Params(const std::vector>& args) { auto has1 = [](const auto& v) { return (v.size() > 1) && !v[1].empty();}; auto has2 = [](const auto& v) { return (v.size() > 2) && !v[2].empty();}; @@ -44,7 +44,7 @@ Params::Params(const std::vector>& args) bool ok = false; if ((arg[0] == "host") && has1(arg)) { - const char* address = arg[1].data(); + const char* address = arg[1].c_str(); if (m_hosts.empty()) { m_hosts.emplace_back(); @@ -63,7 +63,7 @@ Params::Params(const std::vector>& args) m_hosts.emplace_back(); } - m_hosts.back().m_rpcPort = static_cast(std::min(std::max(strtoul(arg[1].data(), nullptr, 10), 1UL), 65535UL)); + m_hosts.back().m_rpcPort = static_cast(std::min(std::max(strtoul(arg[1].c_str(), nullptr, 10), 1UL), 65535UL)); ok = true; } @@ -72,7 +72,7 @@ Params::Params(const std::vector>& args) m_hosts.emplace_back(); } - m_hosts.back().m_zmqPort = static_cast(std::min(std::max(strtoul(arg[1].data(), nullptr, 10), 1UL), 65535UL)); + m_hosts.back().m_zmqPort = static_cast(std::min(std::max(strtoul(arg[1].c_str(), nullptr, 10), 1UL), 65535UL)); ok = true; } @@ -82,7 +82,7 @@ Params::Params(const std::vector>& args) } if ((arg[0] == "wallet") && has1(arg)) { - const char* s = arg[1].data(); + const char* s = arg[1].c_str(); if (!m_mainWallet.decode(s)) { LOGERR(1, "Wallet " << s << " failed to decode"); @@ -92,7 +92,7 @@ Params::Params(const std::vector>& args) } if ((arg[0] == "subaddress") && has1(arg)) { - const char* s = arg[1].data(); + const char* s = arg[1].c_str(); if (!m_subaddress.decode(s)) { LOGERR(1, "Subaddress " << s << " failed to decode"); @@ -107,7 +107,7 @@ Params::Params(const std::vector>& args) } if ((arg[0] == "stratum-ban-time") && has1(arg)) { - m_stratumBanTime = strtoull(arg[1].data(), nullptr, 10); + m_stratumBanTime = strtoull(arg[1].c_str(), nullptr, 10); ok = true; } @@ -122,7 +122,7 @@ Params::Params(const std::vector>& args) } if ((arg[0] == "loglevel") && has1(arg)) { - const int level = std::min(std::max(static_cast(strtol(arg[1].data(), nullptr, 10)), 0), log::MAX_GLOBAL_LOG_LEVEL); + const int level = std::min(std::max(static_cast(strtol(arg[1].c_str(), nullptr, 10)), 0), log::MAX_GLOBAL_LOG_LEVEL); log::GLOBAL_LOG_LEVEL = level; ok = true; } @@ -170,17 +170,17 @@ Params::Params(const std::vector>& args) #endif if (((arg[0] == "out-peers") || (arg[0] == "outpeers")) && has1(arg)) { - m_maxOutgoingPeers = std::min(std::max(strtoul(arg[1].data(), nullptr, 10), 10UL), 450UL); + m_maxOutgoingPeers = std::min(std::max(strtoul(arg[1].c_str(), nullptr, 10), 10UL), 450UL); ok = true; } if (((arg[0] == "in-peers") || (arg[0] == "inpeers")) && has1(arg)) { - m_maxIncomingPeers = std::min(std::max(strtoul(arg[1].data(), nullptr, 10), 10UL), 450UL); + m_maxIncomingPeers = std::min(std::max(strtoul(arg[1].c_str(), nullptr, 10), 10UL), 450UL); ok = true; } if ((arg[0] == "start-mining") && has1(arg)) { - m_minerThreads = std::min(std::max(strtoul(arg[1].data(), nullptr, 10), 1UL), 64UL); + m_minerThreads = std::min(std::max(strtoul(arg[1].c_str(), nullptr, 10), 1UL), 64UL); ok = true; } @@ -240,7 +240,7 @@ Params::Params(const std::vector>& args) } if ((arg[0] == "p2p-external-port") && has1(arg)) { - m_p2pExternalPort = static_cast(std::min(std::max(strtoul(arg[1].data(), nullptr, 10), 1UL), 65535UL)); + m_p2pExternalPort = static_cast(std::min(std::max(strtoul(arg[1].c_str(), nullptr, 10), 1UL), 65535UL)); ok = true; } @@ -301,7 +301,7 @@ Params::Params(const std::vector>& args) } if (!ok) { - fprintf(stderr, "Unknown or invalid command line parameter \"%s\"\n\n", arg[0].data()); + fprintf(stderr, "Unknown or invalid command line parameter \"%s\"\n\n", arg[0].c_str()); p2pool_usage(); throw std::exception(); } diff --git a/src/params.h b/src/params.h index 340b466..623b176 100644 --- a/src/params.h +++ b/src/params.h @@ -29,7 +29,7 @@ struct Params FORCEINLINE Params() {} #endif - Params(const std::vector>& args); + explicit Params(const std::vector>& args); bool valid() const; diff --git a/src/util.cpp b/src/util.cpp index 45d3e84..be03217 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -27,6 +27,7 @@ extern "C" { #include #include #include +#include #if !defined(_WIN32) && defined(HAVE_SCHED) #include @@ -1033,4 +1034,114 @@ hash from_onion_v3(const std::string& address) return result; } +std::vector> parse_config(const std::string& file_name) +{ + std::ifstream f(file_name); + + if (!f.is_open() || !f.good()) { + throw std::exception(); + } + + std::vector> args; + + auto is_alpha = [](char c) { return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'); }; + auto is_num = [](char c) { return ('0' <= c && c <= '9'); }; + + std::string s; + + while (std::getline(f, s).good()) { + if (s.empty()) { + continue; + } + + bool quoted = false; + + enum { + BEFORE_NAME, + COMMENT, + NAME, + AFTER_NAME, + VALUE, + VALUE_ESCAPE_CHAR, + } state = BEFORE_NAME; + + for (const char c : s) { + switch (state) { + case BEFORE_NAME: + if (is_alpha(c)) { + state = NAME; + args.emplace_back(std::vector(1, std::string(1, c))); + } + else if (c == '#') { + state = COMMENT; + } + break; + + case COMMENT: + break; + + case NAME: + if (is_alpha(c) || is_num(c) || (c == '-')) { + args.back()[0] += c; + } + else if (c == '#') { + state = COMMENT; + } + else { + state = AFTER_NAME; + } + break; + + case AFTER_NAME: + if (c == '#') { + state = COMMENT; + } + else if ((c != '\t') && (c != ' ') && (c != '=')) { + state = VALUE; + quoted = (c == '"'); + if (quoted) { + args.back().emplace_back(std::string()); + } + else { + args.back().emplace_back(std::string(1, c)); + } + } + break; + + case VALUE: + if (quoted) { + if (c == '"') { + state = AFTER_NAME; + } + else if (c == '\\') { + state = VALUE_ESCAPE_CHAR; + } + else { + args.back().back() += c; + } + } + else { + if (c == ' ') { + state = AFTER_NAME; + } + else if (c == '#') { + state = COMMENT; + } + else { + args.back().back() += c; + } + } + break; + + case VALUE_ESCAPE_CHAR: + args.back().back() += c; + state = VALUE; + break; + } + } + } + + return args; +} + } // namespace p2pool diff --git a/src/util.h b/src/util.h index 6cdad5b..ce0c9fe 100644 --- a/src/util.h +++ b/src/util.h @@ -441,6 +441,8 @@ static FORCEINLINE constexpr hash from_onion_v3_const(const char* address) return result; } +std::vector> parse_config(const std::string& file_name); + } // namespace p2pool void memory_tracking_start(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 78a140d..fb8f2ee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -191,3 +191,4 @@ add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMA add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/src/sidechain_dump.dat.xz" $) add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/src/sidechain_dump_mini.dat.xz" $) add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/src/sidechain_dump_nano.dat.xz" $) +add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/src/test_params.conf" $) diff --git a/tests/src/test_params.conf b/tests/src/test_params.conf new file mode 100644 index 0000000..6aea144 --- /dev/null +++ b/tests/src/test_params.conf @@ -0,0 +1,17 @@ +# comment 1 + +param1=value1 +param2 = value2 # comment 2 +param3 value3#_comment_3 + +param4 = "value with spaces # not a comment" # a real comment +param5 = "spaces and quotes \" and \\ slashes" + +param6 = value1 value2 value3 + + param7=value7 + +param8#comment +param9 + +abcdefghijklmnopqrstuvwxyz-ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789=0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ-abcdefghijklmnopqrstuvwxyz=:// diff --git a/tests/src/util_tests.cpp b/tests/src/util_tests.cpp index 0991724..edb8100 100644 --- a/tests/src/util_tests.cpp +++ b/tests/src/util_tests.cpp @@ -247,4 +247,65 @@ TEST(util, onion) ASSERT_TRUE(from_onion_v3("civ5tgldg3yx73ytse6hvvk3nm6q3zctbqvytpszihm35b33ze73kxad.onion").empty()); } +TEST(util, parse_config) +{ + const std::vector> args = parse_config("test_params.conf"); + + ASSERT_EQ(args.size(), 10); + + ASSERT_EQ(args[0].size(), 2); + ASSERT_EQ(args[0][0], "param1"); + ASSERT_EQ(args[0][1], "value1"); + + ASSERT_EQ(args[1].size(), 2); + ASSERT_EQ(args[1][0], "param2"); + ASSERT_EQ(args[1][1], "value2"); + + ASSERT_EQ(args[2].size(), 2); + ASSERT_EQ(args[2][0], "param3"); + ASSERT_EQ(args[2][1], "value3"); + + ASSERT_EQ(args[3].size(), 2); + ASSERT_EQ(args[3][0], "param4"); + ASSERT_EQ(args[3][1], "value with spaces # not a comment"); + + ASSERT_EQ(args[4].size(), 2); + ASSERT_EQ(args[4][0], "param5"); + ASSERT_EQ(args[4][1], "spaces and quotes \" and \\ slashes"); + + ASSERT_EQ(args[5].size(), 4); + ASSERT_EQ(args[5][0], "param6"); + ASSERT_EQ(args[5][1], "value1"); + ASSERT_EQ(args[5][2], "value2"); + ASSERT_EQ(args[5][3], "value3"); + + ASSERT_EQ(args[6].size(), 2); + ASSERT_EQ(args[6][0], "param7"); + ASSERT_EQ(args[6][1], "value7"); + + ASSERT_EQ(args[7].size(), 1); + ASSERT_EQ(args[7][0], "param8"); + + ASSERT_EQ(args[8].size(), 1); + ASSERT_EQ(args[8][0], "param9"); + + ASSERT_EQ(args[9].size(), 2); + ASSERT_EQ(args[9][0], "abcdefghijklmnopqrstuvwxyz-ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789"); + ASSERT_EQ(args[9][1], "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ-abcdefghijklmnopqrstuvwxyz=://"); + + bool exception_thrown = false; + + try { + parse_config("non_existent_file.conf"); + + // Execution shouldn't reach this line + FAIL(); + } + catch (const std::exception&) { + exception_thrown = true; + } + + ASSERT_TRUE(exception_thrown); +} + }