♻️ refactor(cli): validate startup arguments

This commit is contained in:
csh
2026-07-11 18:07:22 +08:00
parent 9416d44d3d
commit 3642aa8b5b
7 changed files with 368 additions and 106 deletions
+37 -10
View File
@@ -8,18 +8,43 @@ import std;
import lsp.core.server;
import lsp.utils.args_parser;
namespace
{
void SetupLogger(const lsp::utils::ServerConfig& config)
{
spdlog::set_pattern("%Y-%m-%d %H:%M:%S.%e [%t] [%^%l%$] %v");
auto logger = config.log_file.empty() ? spdlog::stderr_logger_mt("console_logger") : spdlog::basic_logger_mt("file_logger", config.log_file);
logger->set_level(config.log_level);
spdlog::set_default_logger(logger);
spdlog::set_level(config.log_level);
}
}
export int Run(int argc, char* argv[])
{
lsp::utils::ArgsParser& args_parser = lsp::utils::ArgsParser::Instance();
auto& config = args_parser.Parse(argc, argv);
if (config.show_help)
auto parsed = lsp::utils::ParseArgs(argc, argv);
if (!parsed)
{
lsp::utils::ArgsParser::PrintHelp(argv[0]);
std::cerr << "[TSL-LSP] Argument error: " << parsed.error() << '\n';
return 2;
}
if (parsed->action == lsp::utils::ParseAction::kShowHelp)
{
lsp::utils::PrintHelp(std::cout, argv[0]);
return 0;
}
lsp::utils::ArgsParser::SetupLogger(config);
const auto& config = parsed->config;
try
{
SetupLogger(config);
}
catch (const std::exception& error)
{
std::cerr << "[TSL-LSP] Failed to initialize logger: " << error.what() << '\n';
return 1;
}
try
{
@@ -27,16 +52,18 @@ export int Run(int argc, char* argv[])
lsp::core::LspServer server(config.thread_count, config.interpreter_path);
server.Run();
}
catch (const std::exception& e)
catch (const std::exception& error)
{
std::cerr << "[TSL-LSP] Server fatal error: " << e.what() << std::endl;
spdlog::error("Server fatal error: {}", e.what());
std::cerr << "[TSL-LSP] Server fatal error: " << error.what() << '\n';
spdlog::error("Server fatal error: {}", error.what());
spdlog::shutdown();
return 1;
}
catch (...)
{
std::cerr << "[TSL-LSP] Server unknown fatal error" << std::endl;
std::cerr << "[TSL-LSP] Server unknown fatal error\n";
spdlog::error("Server unknown fatal error");
spdlog::shutdown();
return 1;
}
+86 -96
View File
@@ -1,141 +1,131 @@
module;
export module lsp.utils.args_parser;
import spdlog;
import spdlog;
import std;
export namespace lsp::utils
{
struct ServerConfig
{
bool use_stderr = false;
bool show_help = false;
std::size_t thread_count = 4;
spdlog::level::level_enum log_level = spdlog::level::info;
std::string log_file;
std::string interpreter_path;
};
class ArgsParser
enum class ParseAction
{
public:
ArgsParser(const ArgsParser&) = delete;
ArgsParser& operator=(const ArgsParser&) = delete;
static ArgsParser& Instance();
const ServerConfig& Parse(int argc, char* argv[]);
const ServerConfig& GetConfig() const;
static void SetupLogger(const ServerConfig& config);
static void PrintHelp(const std::string& program_name);
private:
ArgsParser() = default;
~ArgsParser() = default;
ServerConfig config_;
kRun,
kShowHelp,
};
struct ParseResult
{
ParseAction action = ParseAction::kRun;
ServerConfig config;
};
std::expected<ParseResult, std::string> ParseArgs(int argc, char* const argv[]);
void PrintHelp(std::ostream& output, std::string_view program_name);
}
namespace lsp::utils
{
ArgsParser& ArgsParser::Instance()
namespace
{
static ArgsParser instance;
return instance;
constexpr std::size_t kMinThreadCount = 1;
constexpr std::size_t kMaxThreadCount = 256;
std::optional<spdlog::level::level_enum> ParseLogLevel(std::string_view value)
{
if (value == "trace")
return spdlog::level::trace;
if (value == "debug")
return spdlog::level::debug;
if (value == "info")
return spdlog::level::info;
if (value == "warn")
return spdlog::level::warn;
if (value == "error")
return spdlog::level::err;
if (value == "off")
return spdlog::level::off;
return std::nullopt;
}
}
const ServerConfig& ArgsParser::Parse(int argc, char* argv[])
std::expected<ParseResult, std::string> ParseArgs(int argc, char* const argv[])
{
config_ = ServerConfig{};
// Default to stderr so LSP stdio (stdout) stays clean.
config_.use_stderr = true;
for (int i = 1; i < argc; ++i)
{
if (std::string_view(argv[i]) == "--help")
return ParseResult{ .action = ParseAction::kShowHelp, .config = {} };
}
ParseResult result;
constexpr std::string_view kLogPrefix = "--log=";
constexpr std::string_view kLogFilePrefix = "--log-file=";
constexpr std::string_view kThreadsPrefix = "--threads=";
constexpr std::string_view kInterpreterPrefix = "--interpreter=";
for (int i = 1; i < argc; ++i)
{
std::string arg = argv[i];
if (arg == "--help")
const std::string_view argument = argv[i];
if (argument.starts_with(kLogPrefix))
{
config_.show_help = true;
return config_;
const auto value = argument.substr(kLogPrefix.size());
auto level = ParseLogLevel(value);
if (!level)
return std::unexpected("Invalid value for --log: '" +
std::string(value) + "'");
result.config.log_level = *level;
}
if (arg == "--log=trace")
config_.log_level = spdlog::level::trace;
else if (arg == "--log=debug")
config_.log_level = spdlog::level::debug;
else if (arg == "--log=info")
config_.log_level = spdlog::level::info;
else if (arg == "--log=warn")
config_.log_level = spdlog::level::warn;
else if (arg == "--log=error")
config_.log_level = spdlog::level::err;
else if (arg == "--log=off")
config_.log_level = spdlog::level::off;
else if (arg == "--log-stderr")
config_.use_stderr = true;
else if (arg == "--log-stdout")
config_.use_stderr = false;
else if (arg.starts_with(kLogFilePrefix))
config_.log_file = arg.substr(kLogFilePrefix.size());
else if (arg == "--use-stdio")
config_.use_stderr = true;
else if (arg.starts_with(kThreadsPrefix))
else if (argument.starts_with(kLogFilePrefix))
{
auto value = arg.substr(kThreadsPrefix.size());
config_.thread_count = std::max<std::size_t>(1, static_cast<std::size_t>(std::stoi(value)));
const auto value = argument.substr(kLogFilePrefix.size());
if (value.empty())
return std::unexpected("--log-file requires a non-empty path");
result.config.log_file = value;
}
else if (arg.starts_with(kInterpreterPrefix))
else if (argument.starts_with(kThreadsPrefix))
{
config_.interpreter_path = arg.substr(kInterpreterPrefix.size());
const auto value = argument.substr(kThreadsPrefix.size());
std::size_t count = 0;
const auto [end, error] =
std::from_chars(value.data(), value.data() + value.size(), count);
if (error != std::errc{} || end != value.data() + value.size() ||
count < kMinThreadCount || count > kMaxThreadCount)
{
return std::unexpected("Invalid value for --threads: '" +
std::string(value) + "' (expected 1..256)");
}
result.config.thread_count = count;
}
else if (argument.starts_with(kInterpreterPrefix))
{
const auto value = argument.substr(kInterpreterPrefix.size());
if (value.empty())
return std::unexpected("--interpreter requires a non-empty path");
result.config.interpreter_path = value;
}
else
{
return std::unexpected("Unknown argument: " + std::string(argument));
}
}
return config_;
return result;
}
const ServerConfig& ArgsParser::GetConfig() const
void PrintHelp(std::ostream& output, std::string_view program_name)
{
return config_;
}
void ArgsParser::SetupLogger(const ServerConfig& config)
{
spdlog::set_pattern("%Y-%m-%d %H:%M:%S.%e [%t] [%^%l%$] %v");
if (!config.log_file.empty())
{
auto file_logger = spdlog::basic_logger_mt("file_logger", config.log_file);
file_logger->set_level(config.log_level);
spdlog::set_default_logger(file_logger);
}
else
{
auto console_logger = config.use_stderr ? spdlog::stderr_logger_mt("console_logger") : spdlog::stdout_logger_mt("console_logger");
console_logger->set_level(config.log_level);
spdlog::set_default_logger(console_logger);
}
spdlog::set_level(config.log_level);
}
void ArgsParser::PrintHelp(const std::string& program_name)
{
std::cout << "Usage: " << program_name << " [options]\\n\\n"
<< "Options:\\n"
<< " --help Show this help message\\n"
<< " --log=<level> Set log level (trace, debug, info, warn, error, off)\\n"
<< " --log-stderr Output logs to stderr (default)\\n"
<< " --log-stdout Output logs to stdout\\n"
<< " --log-file=<path> Output logs to specified file\\n"
<< " --use-stdio Alias for --log-stderr (keep stdout clean for LSP)\\n"
<< " --threads=<count> Number of worker threads\\n"
<< " --interpreter=<path> Custom interpreter path\\n";
output << "Usage: " << program_name << " [options]\n\n"
<< "Options:\n"
<< " --help Show this help message\n"
<< " --log=<level> Set log level (trace, debug, info, warn, error, off)\n"
<< " --log-file=<path> Output logs to specified file\n"
<< " --threads=<count> Number of worker threads (1-256)\n"
<< " --interpreter=<path> Custom interpreter path\n";
}
}