diff options
| author | isanae <14251494+isanae@users.noreply.github.com> | 2020-11-06 10:51:03 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-11-06 10:51:03 -0500 |
| commit | b909677c3fc6a7b7a1993d341a2bd420715e292a (patch) | |
| tree | 8a883011d5e207c31edf9b94ee67bff1827c2ddf /src/commandline.cpp | |
| parent | a63a27840bfa7b08f295bfe682ebc33d70a613b9 (diff) | |
| parent | ad8e9d99b30578676c15d10a74f010439e132406 (diff) | |
Merge pull request #1280 from isanae/command-line
Command line and instance dialogs
Diffstat (limited to 'src/commandline.cpp')
| -rw-r--r-- | src/commandline.cpp | 593 |
1 files changed, 593 insertions, 0 deletions
diff --git a/src/commandline.cpp b/src/commandline.cpp new file mode 100644 index 00000000..3f8a6b1a --- /dev/null +++ b/src/commandline.cpp @@ -0,0 +1,593 @@ +#include "commandline.h" +#include "env.h" +#include "shared/util.h" + +namespace cl +{ + +std::string pad_right(std::string s, std::size_t n, char c=' ') +{ + if (s.size() < n) + s.append(n - s.size() , c); + + return s; +} + +std::string table( + const std::vector<std::pair<std::string, std::string>>& v, + std::size_t indent, std::size_t spacing) +{ + std::size_t longest = 0; + + for (auto&& p : v) + longest = std::max(longest, p.first.size()); + + std::string s; + + for (auto&& p : v) + { + if (!s.empty()) + s += "\n"; + + s += + std::string(indent, ' ') + + pad_right(p.first, longest) + " " + + std::string(spacing, ' ') + + p.second; + } + + return s; + +} + + +CommandLine::CommandLine() +{ + createOptions(); + m_commands.push_back(std::make_unique<ExeCommand>()); + m_commands.push_back(std::make_unique<RunCommand>()); + m_commands.push_back(std::make_unique<CrashDumpCommand>()); + m_commands.push_back(std::make_unique<LaunchCommand>()); +} + +std::optional<int> CommandLine::run(const std::wstring& line) +{ + try + { + auto args = po::split_winmain(line); + if (!args.empty()) { + // remove program name + args.erase(args.begin()); + } + + auto parsed = po::wcommand_line_parser(args) + .options(m_allOptions) + .positional(m_positional) + .allow_unregistered() + .run(); + + po::store(parsed, m_vm); + po::notify(m_vm); + + auto opts = po::collect_unrecognized( + parsed.options, po::include_positional); + + + if (m_vm.count("command")) { + const auto commandName = m_vm["command"].as<std::string>(); + + for (auto&& c : m_commands) { + if (c->name() == commandName) { + // remove the command name itself + opts.erase(opts.begin()); + + try + { + po::wcommand_line_parser parser(opts); + + auto co = c->allOptions(); + parser.options(co); + + if (c->allow_unregistered()) { + parser.allow_unregistered(); + } + + auto pos = c->positional(); + parser.positional(pos); + + parsed = parser.run(); + + po::store(parsed, m_vm); + po::notify(m_vm); + + if (m_vm.count("help")) { + env::Console console; + std::cout << usage(c.get()) << "\n"; + return 0; + } + + return c->run(line, m_vm, opts); + } + catch(po::error& e) + { + env::Console console; + + std::cerr + << e.what() << "\n" + << usage(c.get()) << "\n"; + + return 1; + } + } + } + } + + if (m_vm.count("help")) { + env::Console console; + std::cout << usage() << "\n"; + return 0; + } + + if (!opts.empty()) { + const auto qs = QString::fromStdWString(opts[0]); + m_shortcut = qs; + + if (!m_shortcut.isValid()) { + if (isNxmLink(qs)) { + m_nxmLink = qs; + } else { + m_executable = qs; + } + } + + // remove the shortcut/nxm/executable + opts.erase(opts.begin()); + + for (auto&& o : opts) { + m_untouched.push_back(QString::fromStdWString(o)); + } + } + + return {}; + } + catch(po::error& e) + { + env::Console console; + + std::cerr + << e.what() << "\n" + << usage() << "\n"; + + return 1; + } +} + +void CommandLine::clear() +{ + m_vm.clear(); + m_shortcut = {}; + m_nxmLink = {}; +} + +void CommandLine::createOptions() +{ + m_visibleOptions.add_options() + ("help", "show this message") + ("multiple", "allow multiple MO processes to run; see below") + ("instance,i", po::value<std::string>(), "use the given instance (defaults to last used)") + ("profile,p", po::value<std::string>(), "use the given profile (defaults to last used)"); + + po::options_description options; + options.add_options() + ("command", po::value<std::string>(), "command") + ("subargs", po::value<std::vector<std::string> >(), "args"); + + m_positional + .add("command", 1) + .add("subargs", -1); + + m_allOptions.add(m_visibleOptions); + m_allOptions.add(options); +} + +std::string CommandLine::usage(const Command* c) const +{ + std::ostringstream oss; + + oss + << "\n" + << "Usage:\n"; + + if (c) { + oss + << " ModOrganizer.exe [global-options] " << c->usageLine() << "\n" + << "\n" + << "Command options:\n" + << c->visibleOptions() << "\n"; + } else { + oss + << " ModOrganizer.exe [options] [[command] [command-options]]\n" + << "\n" + << "Commands:\n"; + + std::vector<std::pair<std::string, std::string>> v; + for (auto&& c : m_commands) { + v.push_back({c->name(), c->description()}); + } + + oss + << table(v, 2, 4) << "\n" + << "\n"; + } + + oss + << "Global options:\n" + << m_visibleOptions << "\n"; + + if (!c) { + oss << "\n" << more() << "\n"; + } + + return oss.str(); +} + +bool CommandLine::multiple() const +{ + return (m_vm.count("multiple") > 0); +} + +std::optional<QString> CommandLine::profile() const +{ + if (m_vm.count("profile")) { + return QString::fromStdString(m_vm["profile"].as<std::string>()); + } + + return {}; +} + +std::optional<QString> CommandLine::instance() const +{ + if (m_shortcut.isValid() && m_shortcut.hasInstance()) { + return m_shortcut.instance(); + } else if (m_vm.count("instance")) { + return QString::fromStdString(m_vm["instance"].as<std::string>()); + } + + return {}; +} + +const MOShortcut& CommandLine::shortcut() const +{ + return m_shortcut; +} + +std::optional<QString> CommandLine::nxmLink() const +{ + return m_nxmLink; +} + +std::optional<QString> CommandLine::executable() const +{ + return m_executable; +} + +const QStringList& CommandLine::untouched() const +{ + return m_untouched; +} + +std::string CommandLine::more() const +{ + return + "Multiple processes\n" + " A note on terminology: 'instance' can either mean an MO process\n" + " that's running on the system, or a set of mods and profiles managed\n" + " by MO. To avoid confusion, the term 'process' is used below for the\n" + " former.\n" + " \n" + " --multiple can be used to allow multiple MO processes to run\n" + " simultaneously. This is unsupported and can create all sorts of weird\n" + " problems. To minimize these:\n" + " \n" + " 1) Never have multiple MO processes running that manage the same\n" + " game instance.\n" + " 2) If an executable is launched from an MO process, only this\n" + " process may launch executables until all processes are \n" + " terminated.\n" + " \n" + " It is recommended to close _all_ MO processes as soon as multiple\n" + " processes become unnecessary."; +} + + +std::string Command::name() const +{ + return meta().name; +} + +std::string Command::description() const +{ + return meta().description; +} + +std::string Command::usageLine() const +{ + return name() + " " + getUsageLine(); +} + +bool Command::allow_unregistered() const +{ + return false; +} + +po::options_description Command::allOptions() const +{ + po::options_description d; + + d.add(visibleOptions()); + d.add(getInternalOptions()); + + return d; +} + +po::options_description Command::visibleOptions() const +{ + po::options_description d(getVisibleOptions()); + + d.add_options() + ("help", "shows this message"); + + return d; +} + +po::positional_options_description Command::positional() const +{ + return getPositional(); +} + +std::string Command::getUsageLine() const +{ + return "[options]"; +} + +po::options_description Command::getVisibleOptions() const +{ + // no-op + return {}; +} + +po::options_description Command::getInternalOptions() const +{ + // no-op + return {}; +} + +po::positional_options_description Command::getPositional() const +{ + // no-op + return {}; +} + + +std::string Command::usage() const +{ + std::ostringstream oss; + + oss + << "\n" + << "Usage:\n" + << " ModOrganizer.exe [options] [[command] [command-options]]\n" + << "\n" + << "Options:\n" + << visibleOptions() << "\n"; + + return oss.str(); +} + +std::optional<int> Command::run( + const std::wstring& originalLine, + po::variables_map vm, + std::vector<std::wstring> untouched) +{ + m_original = originalLine; + m_vm = vm; + m_untouched = untouched; + + return doRun(); +} + +const std::wstring& Command::originalCmd() const +{ + return m_original; +} + +const po::variables_map& Command::vm() const +{ + return m_vm; +} + +const std::vector<std::wstring>& Command::untouched() const +{ + return m_untouched; +} + + +po::options_description CrashDumpCommand::getVisibleOptions() const +{ + po::options_description d; + + d.add_options() + ("type", po::value<std::string>()->default_value("mini"), "mini|data|full"); + + return d; +} + +Command::Meta CrashDumpCommand::meta() const +{ + return {"crashdump", "writes a crashdump for a running process of MO"}; +} + +std::optional<int> CrashDumpCommand::doRun() +{ + env::Console console; + + const auto typeString = vm()["type"].as<std::string>(); + const auto type = env::coreDumpTypeFromString(typeString); + + // dump + const auto b = env::coredumpOther(type); + if (!b) { + std::wcerr << L"\n>>>> a minidump file was not written\n\n"; + } + + std::wcerr << L"Press enter to continue..."; + std::wcin.get(); + + return (b ? 0 : 1); +} + + +bool LaunchCommand::allow_unregistered() const +{ + return true; +} + +Command::Meta LaunchCommand::meta() const +{ + return {"launch", "(internal, do not use)"}; +} + +std::optional<int> LaunchCommand::doRun() +{ + // needs at least the working directory and process name + if (untouched().size() < 2) { + return 1; + } + + std::vector<std::wstring> arg; + auto args = UntouchedCommandLineArguments(2, arg); + + return SpawnWaitProcess(arg[1].c_str(), args); +} + +int LaunchCommand::SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine) +{ + PROCESS_INFORMATION pi{ 0 }; + STARTUPINFO si{ 0 }; + si.cb = sizeof(si); + std::wstring commandLineCopy = commandLine; + + if (!CreateProcessW(NULL, &commandLineCopy[0], NULL, NULL, FALSE, 0, NULL, workingDirectory, &si, &pi)) { + // A bit of a problem where to log the error message here, at least this way you can get the message + // using a either DebugView or a live debugger: + std::wostringstream ost; + ost << L"CreateProcess failed: " << commandLine << ", " << GetLastError(); + OutputDebugStringW(ost.str().c_str()); + return -1; + } + + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exitCode = (DWORD)-1; + ::GetExitCodeProcess(pi.hProcess, &exitCode); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return static_cast<int>(exitCode); +} + +// Parses the first parseArgCount arguments of the current process command line and returns +// them in parsedArgs, the rest of the command line is returned untouched. +LPCWSTR LaunchCommand::UntouchedCommandLineArguments( + int parseArgCount, std::vector<std::wstring>& parsedArgs) +{ + LPCWSTR cmd = GetCommandLineW(); + LPCWSTR arg = nullptr; // to skip executable name + for (; parseArgCount >= 0 && *cmd; ++cmd) + { + if (*cmd == '"') { + int escaped = 0; + for (++cmd; *cmd && (*cmd != '"' || escaped % 2 != 0); ++cmd) + escaped = *cmd == '\\' ? escaped + 1 : 0; + } + if (*cmd == ' ') { + if (arg) + if (cmd-1 > arg && *arg == '"' && *(cmd-1) == '"') + parsedArgs.push_back(std::wstring(arg+1, cmd-1)); + else + parsedArgs.push_back(std::wstring(arg, cmd)); + arg = cmd + 1; + --parseArgCount; + } + } + return cmd; +} + + +std::string ExeCommand::getUsageLine() const +{ + return "[options] exe-name"; +} + +po::options_description ExeCommand::getVisibleOptions() const +{ + po::options_description d; + + d.add_options() + ("arguments,a", po::value<std::string>()->default_value(""), "override arguments") + ("cwd,c", po::value<std::string>()->default_value(""), "override working directory"); + + return d; +} + +po::options_description ExeCommand::getInternalOptions() const +{ + po::options_description d; + + d.add_options() + ("exe-name", po::value<std::string>()->required(), "executable name"); + + return d; +} + +po::positional_options_description ExeCommand::getPositional() const +{ + po::positional_options_description d; + + d.add("exe-name", 1); + + return d; +} + +Command::Meta ExeCommand::meta() const +{ + return {"exe", "launches a configured executable"}; +} + +std::optional<int> ExeCommand::doRun() +{ + const auto exe = vm()["exe-name"].as<std::string>(); + const auto args = vm()["arguments"].as<std::string>(); + const auto cwd = vm()["cwd"].as<std::string>(); + + + + return 0; +} + + +po::options_description RunCommand::getOptions() const +{ + return {}; +} + +Command::Meta RunCommand::meta() const +{ + return {"run", "launches an arbitrary program"}; +} + +std::optional<int> RunCommand::doRun() +{ + return {}; +} + +} // namespace |
