#include "commandline.h"
#include "env.h"
#include "instancemanager.h"
#include "loglist.h"
#include "messagedialog.h"
#include "multiprocess.h"
#include "organizercore.h"
#include "shared/appconfig.h"
#include "shared/util.h"
#include <log.h>
#include <report.h>
namespace cl
{
using namespace MOBase;
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() : m_command(nullptr)
{
createOptions();
add<RunCommand, ReloadPluginCommand, DownloadFileCommand, RefreshCommand,
CrashDumpCommand, LaunchCommand>();
}
std::optional<int> CommandLine::process(const std::wstring& line)
{
try {
auto args = po::split_winmain(line);
if (!args.empty()) {
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) {
opts.erase(opts.begin());
try {
if (!c->legacy()) {
po::wcommand_line_parser parser(opts);
auto co = c->allOptions();
parser.options(co);
auto pos = c->positional();
parser.positional(pos);
parsed = parser.run();
po::store(parsed, m_vm);
if (m_vm.count("help")) {
env::Console console;
std::cout << usage(c.get()) << "\n";
return 0;
}
po::notify(m_vm);
}
c->set(line, m_vm, opts);
m_command = c.get();
return runEarly();
} 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]);
if (qs.startsWith("--")) {
env::Console console;
std::cerr << "\nUnrecognized option " << qs.toStdString() << "\n";
return 1;
}
m_shortcut = qs;
if (!m_shortcut.isValid()) {
if (isNxmLink(qs)) {
m_nxmLink = qs;
} else {
m_executable = qs;
}
}
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;
}
}
bool CommandLine::forwardToPrimary(MOMultiProcess& multiProcess)
{
if (m_shortcut.isValid()) {
multiProcess.sendMessage(m_shortcut.toString());
} else if (m_nxmLink) {
multiProcess.sendMessage(*m_nxmLink);
} else if (m_command && m_command->canForwardToPrimary()) {
multiProcess.sendMessage(QString::fromWCharArray(GetCommandLineW()));
} else {
return false;
}
return true;
}
std::optional<int> CommandLine::runEarly()
{
if (m_vm.count("logs")) {
logToStdout(true);
}
if (m_command) {
return m_command->runEarly();
}
return {};
}
std::optional<int> CommandLine::runPostApplication(MOApplication& a)
{
if (m_vm.count("instance") && m_vm["instance"].as<std::string>() == "") {
env::Console c;
if (auto i = InstanceManager::singleton().currentInstance()) {
std::cout << i->displayName().toStdString() << "\n";
} else {
std::cout << "no instance configured\n";
}
return 0;
}
if (m_command) {
return m_command->runPostApplication(a);
}
return {};
}
std::optional<int> CommandLine::runPostMultiProcess(MOMultiProcess& mp)
{
if (m_command) {
return m_command->runPostMultiProcess(mp);
}
return {};
}
std::optional<int> CommandLine::runPostOrganizer(OrganizerCore& core)
{
if (m_shortcut.isValid()) {
if (m_shortcut.hasExecutable()) {
try {
core.processRunner()
.setFromShortcut(m_shortcut)
.setWaitForCompletion(ProcessRunner::ForCommandLine, UILocker::PreventExit)
.run();
return 0;
} catch (std::exception&) {
return 1;
}
}
} else if (m_nxmLink) {
log::debug("starting download from command line: {}", *m_nxmLink);
core.downloadRequestedNXM(*m_nxmLink);
} else if (m_executable) {
const QString exeName = *m_executable;
log::debug("starting {} from command line", exeName);
try {
core.processRunner()
.setFromFileOrExecutable(exeName, m_untouched)
.setWaitForCompletion(ProcessRunner::ForCommandLine, UILocker::PreventExit)
.run();
return 0;
} catch (const std::exception& e) {
reportError(QObject::tr("failed to start application: %1").arg(e.what()));
return 1;
}
} else if (m_command) {
return m_command->runPostOrganizer(core);
}
return {};
}
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")
("pick", "show the select instance dialog on startup")
("logs", "duplicates the logs to stdout")
("instance,i", po::value<std::string>()->implicit_value(""),
"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";
const std::string more = c->moreInfo();
if (!more.empty()) {
oss << more << "\n\n";
}
oss << "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) {
if (c->legacy()) {
continue;
}
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::pick() const
{
return (m_vm.count("pick") > 0);
}
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.instanceName();
} 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.";
}
Command::Meta::Meta(std::string n, std::string d, std::string u, std::string m)
: name(n), description(d), usage(u), more(m)
{}
std::string Command::name() const
{
return meta().name;
}
std::string Command::description() const
{
return meta().description;
}
std::string Command::usageLine() const
{
return name() + " " + meta().usage;
}
std::string Command::moreInfo() const
{
return meta().more;
}
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();
}
bool Command::legacy() const
{
return false;
}
po::options_description Command::getVisibleOptions() const
{
return {};
}
po::options_description Command::getInternalOptions() const
{
return {};
}
po::positional_options_description Command::getPositional() const
{
return {};
}
void Command::set(const std::wstring& originalLine, po::variables_map vm,
std::vector<std::wstring> untouched)
{
m_original = originalLine;
m_vm = vm;
m_untouched = untouched;
}
std::optional<int> Command::runEarly()
{
return {};
}
std::optional<int> Command::runPostApplication(MOApplication& a)
{
return {};
}
std::optional<int> Command::runPostMultiProcess(MOMultiProcess&)
{
return {};
}
std::optional<int> Command::runPostOrganizer(OrganizerCore&)
{
return {};
}
bool Command::canForwardToPrimary() const
{
return false;
}
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", "[options]",
""};
}
std::optional<int> CrashDumpCommand::runEarly()
{
env::Console console;
const auto typeString = vm()["type"].as<std::string>();
const auto type = env::coreDumpTypeFromString(typeString);
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);
}
Command::Meta LaunchCommand::meta() const
{
return {"launch", "(internal, do not use)", "", ""};
}
bool LaunchCommand::legacy() const
{
return true;
}
std::optional<int> LaunchCommand::runEarly()
{
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)) {
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);
}
LPCWSTR
LaunchCommand::UntouchedCommandLineArguments(int parseArgCount,
std::vector<std::wstring>& parsedArgs)
{
LPCWSTR cmd = GetCommandLineW();
LPCWSTR arg = nullptr;
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;
}
Command::Meta RunCommand::meta() const
{
return {"run", "runs a program, file or a configured executable", "[options] NAME",
"Runs a program or a file with the virtual filesystem. If NAME is a path\n"
"to a non-executable file, the program that is associated with the file\n"
"extension is run instead. With -e, NAME must refer to the name of an\n"
"executable in the instance (for example, \"SKSE\")."};
}
po::options_description RunCommand::getVisibleOptions() const
{
po::options_description d;
d.add_options()("executable,e",
po::value<bool>()->default_value(false)->zero_tokens(),
"the name is a configured executable name")(
"arguments,a", po::value<std::string>(), "override arguments")(
"cwd,c", po::value<std::string>(), "override working directory");
return d;
}
po::options_description RunCommand::getInternalOptions() const
{
po::options_description d;
d.add_options()("NAME", po::value<std::string>()->required(),
"program or executable name");
return d;
}
po::positional_options_description RunCommand::getPositional() const
{
po::positional_options_description d;
d.add("NAME", 1);
return d;
}
bool RunCommand::canForwardToPrimary() const
{
return true;
}
std::optional<int> RunCommand::runPostOrganizer(OrganizerCore& core)
{
const auto program = QString::fromStdString(vm()["NAME"].as<std::string>());
try {
auto p = core.processRunner();
if (vm()["executable"].as<bool>()) {
const auto& exes = *core.executablesList();
auto itor = exes.find(program, true);
if (itor == exes.end()) {
itor = exes.find(program, false);
if (itor == exes.end()) {
reportError(
QObject::tr("Executable '%1' not found in instance '%2'.")
.arg(program)
.arg(InstanceManager::singleton().currentInstance()->displayName()));
return 1;
}
}
p.setFromExecutable(*itor);
} else {
p.setFromFile(nullptr, QFileInfo(program));
}
if (vm().count("arguments")) {
p.setArguments(QString::fromStdString(vm()["arguments"].as<std::string>()));
}
if (vm().count("cwd")) {
p.setCurrentDirectory(QString::fromStdString(vm()["cwd"].as<std::string>()));
}
p.setWaitForCompletion(ProcessRunner::ForCommandLine, UILocker::PreventExit);
const auto r = p.run();
if (r == ProcessRunner::Error) {
reportError(
QObject::tr("Failed to run '%1'. The logs might have more information.")
.arg(program));
return 1;
}
return 0;
} catch (const std::exception& e) {
reportError(
QObject::tr("Failed to run '%1'. The logs might have more information. %2")
.arg(program)
.arg(e.what()));
return 1;
}
}
Command::Meta ReloadPluginCommand::meta() const
{
return {"reload-plugin", "reloads the given plugin", "PLUGIN", ""};
}
po::options_description ReloadPluginCommand::getInternalOptions() const
{
po::options_description d;
d.add_options()("PLUGIN", po::value<std::string>()->required(), "plugin name");
return d;
}
po::positional_options_description ReloadPluginCommand::getPositional() const
{
po::positional_options_description d;
d.add("PLUGIN", 1);
return d;
}
bool ReloadPluginCommand::canForwardToPrimary() const
{
return true;
}
std::optional<int> ReloadPluginCommand::runPostOrganizer(OrganizerCore& core)
{
const QString name = QString::fromStdString(vm()["PLUGIN"].as<std::string>());
QString filepath =
QDir(qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()))
.absoluteFilePath(name);
log::debug("reloading plugin from {}", filepath);
core.pluginContainer().reloadPlugin(filepath);
return {};
}
Command::Meta DownloadFileCommand::meta() const
{
return {"download", "downloads a file", "URL", ""};
}
po::options_description DownloadFileCommand::getInternalOptions() const
{
po::options_description d;
d.add_options()("URL", po::value<std::string>()->required(), "file URL");
return d;
}
po::positional_options_description DownloadFileCommand::getPositional() const
{
po::positional_options_description d;
d.add("URL", 1);
return d;
}
bool DownloadFileCommand::canForwardToPrimary() const
{
return true;
}
std::optional<int> DownloadFileCommand::runPostOrganizer(OrganizerCore& core)
{
const QString url = QString::fromStdString(vm()["URL"].as<std::string>());
if (!url.startsWith("https://")) {
reportError(QObject::tr("Download URL must start with https://"));
return 1;
}
log::debug("starting direct download from command line: {}", url.toStdString());
MessageDialog::showMessage(QObject::tr("Download started"), qApp->activeWindow(),
false);
core.downloadManager()->startDownloadURLs(QStringList() << url);
return {};
}
Command::Meta RefreshCommand::meta() const
{
return {"refresh", "refreshes MO (same as F5)", "", ""};
}
bool RefreshCommand::canForwardToPrimary() const
{
return true;
}
std::optional<int> RefreshCommand::runPostOrganizer(OrganizerCore& core)
{
core.refresh();
return {};
}
}