--- nono/lib/mainapp.cpp 2026/04/29 17:04:28 1.1.1.1 +++ nono/lib/mainapp.cpp 2026/04/29 17:05:05 1.1.1.11 @@ -1,111 +1,682 @@ // // nono -// Copyright (C) 2018 isaki@NetBSD.org +// Copyright (C) 2020 nono project +// Licensed under nono-license.txt // -#include "header.h" #include "mainapp.h" +#include "mystring.h" +#include "netdriver_selector.h" +#include +#include +#include + +// グローバルインスタンス +MainApp gMainApp; + +// ヘルプメッセージ +void +MainApp::ShowHelp(bool all) const +{ + ShowVersion(); + +#define p(msg) printf(" " msg "\n") + + printf("usage: %s []\n", getprogname()); + p("-c vm directory or configuration file"); + p("-f fast mode"); + if (IsGUI()) + p("--fontsize fontsize in monitors {12,16} (default:12)"); + p("-h show brief help message"); + p("--help show all help message including for developers"); + if (IsGUI()) + p("-s,--scale screen scale, default 1.0"); + p("--show-config show configuration variables"); + p("--show-hostnet show list of hostnet drivers"); + p("-v show version"); + p("-V = overwrite config option"); + p("-X [arg..] load and execute host binary (a.out or ELF)"); + + if (all) { + printf("\n(options for developers)\n"); + p("-b [,] set breakpoint"); + p("-B benchmark mode"); + p("-C output log to console"); + p("-d debugger prompt on startup"); + p("-D debugger on console"); + p("-H human68k console emulation"); + p("-L =[,..] set loglevel (-Lhelp displays names)"); + p("--load-only load host binary (a.out or ELF) but not execute"); + p("-M [,..] monitors to display at startup (-Mhelp displays names)"); + } +} + +// long option は enum と struct option をアルファベット順に並べる。 +// enum は getopt() の1文字のオプションと衝突しなければいいので適当に +// 0x80 から始めておく。 +enum { + OPT_fontsize = 0x80, + OPT_load_only, + OPT_help, + OPT_show_config, + OPT_show_config_all, + OPT_show_hostnet, +}; +static struct option longopts[] = { + { "fontsize", required_argument, NULL, OPT_fontsize }, + { "load-only", required_argument, NULL, OPT_load_only }, + { "help", no_argument, NULL, OPT_help }, + { "show-config", no_argument, NULL, OPT_show_config }, + // --show-config-all は開発用なのでヘルプには載せない + { "show-config-all",no_argument, NULL, OPT_show_config_all }, + { "show-hostnet", no_argument, NULL, OPT_show_hostnet }, + { NULL, 0, NULL, 0 }, +}; + +// VM の初期化、ステージ1。 +// VM 作成と設定確定あたりまで。スレッド生成を伴わないもの。 +// 所々 CLI と GUI で処理が違う。 +// 戻り値は以下のいずれか。 +// MainApp::PASS .. 実行を継続(通常パス) +// EXIT_SUCCESS / EXIT_FAILURE .. この終了コードでアプリケーションを終了 +int +MainApp::Init1(bool is_cli_, int ac, char *av[]) +{ + is_cli = is_cli_; + + if (!ParseOpt(ac, av)) { + return EXIT_FAILURE; + } + + // ログ機構は引数処理後なるはや + // CLI ならログは常に標準出力へ(も)出力。 + // GUI なら -C で指定。 + gLogger.UseStdout(IsCLI() ? true : log_to_console); + + // 設定を作成。コンストラクタで初期値を用意 + gConfig.reset(new Config()); + + // 引数で指定されたディレクトリの設定ファイルがあれば読み込んで設定を更新 + ConfigFile file(vmfile); + if (file.Load() == false) { + return EXIT_FAILURE; + } + if (gConfig->Update(file) == false) { + return EXIT_FAILURE; + } + // 最後にコマンドライン引数で更新 + if (gConfig->Update(config_options) == false) { + return EXIT_FAILURE; + } + + // 1. VM 種別を決定 + // VM 種別文字列から vmtype を決定 + const ConfigItem& item = gConfig->Find("vmtype"); + std::string vmstr = string_tolower(item.AsString()); + if (vmstr == "x68030") { + vmtype = VMTYPE_X68030; + } else if (vmstr == "luna") { + vmtype = VMTYPE_LUNA1; + } else if (vmstr == "luna88k") { + vmtype = VMTYPE_LUNA88K; + } else { + if (item.GetFrom() == ConfigItem::FromInitial) { + // 未指定の時 + warnx("vmtype must be specified"); + } else { + // ユーザ由来の時 + item.Err("invalid vmtype"); + } + return EXIT_FAILURE; + } + + // 2. VM 作成 + switch (vmtype) { + case VMTYPE_X68030: + gVM.reset(new VM_X68030()); + break; + + case VMTYPE_LUNA1: + gVM.reset(new VM_LUNA1()); + break; + + case VMTYPE_LUNA88K: + gVM.reset(new VM_LUNA88K()); + break; + + default: + __unreachable(); + } + + // 3. 動的なコンストラクション + if (!gVM->Create()) { + gVM.reset(); + return EXIT_FAILURE; + } + + // デバッガ (ここでもモニタを登録する) + debugger_create(); + + // 設定内容を表示して終了 + // (VM コンストラクタで変数の増減があるのでそれより後、 + // Create() でも SCSI パラメータを減らすので、それより後) + gConfig->Fix(); + if (show_config) { + gConfig->Show(show_config - 1); + gVM.reset(); + return EXIT_SUCCESS; + } -// 高速モード (XXX 設定ファイルと含めて見直すこと) -bool fast_mode; + // 4. ログレベルを設定。コンストラクト後すぐに行う。 + // -L help もここで処理。 + if (!ParseLogopt()) { + gVM.reset(); + return EXIT_FAILURE; + } -// VM ディレクトリ -std::string vmdir; + // 5. ホストネットワークドライバの作成。 + // + // ホストネットワークドライバを作成する時にはログ表示が欲しいが、通常の + // フローではオブジェクトが出揃った後でログレベルを割り当てるので、 + // にわたま問題になってしまう。そこでこれだけ特別対応する。 + // 手順 3. のコンストラクションで EthernetDevice が NetDriverSelector を + // 作成し、手順 4. で通常通りにログレベルの割り当てを受ける。このログを + // 使ってここ(手順5)で実際の NetDriver を作成し、同時にログレベルも引き + // 継ぐことにする。 + if ((bool)gNetDriverSelector) { + // 実際の NetDriver を作成 + NetDriver *newdriver = gNetDriverSelector->InitDriver(); + if (newdriver == NULL) { + gVM.reset(); + return EXIT_FAILURE; + } + // 成功すればセット + gNetDriver.reset(newdriver); -// ログ指定文字列 (-L) -char logopt[256]; + // NetDriverSelector はこれ以降まったく使わないがとりあえず放置。 + } -// 引数 arg のログ指定文字列をパースする。 -// arg は "foo=1,bar=2" 形式の文字列で、これを分解して -// それぞれ担当するオブジェクトのログレベルにセットする。 + return PASS; +} + +// VM の初期化、ステージ2。スレッド生成を伴う。 +// Init1() で引数を受け付けてから Init2() でデバッガスレッドなどを起動するが、 +// -Mhelp とかが指定された場合はここでスレッド開始する前にプロセスを終了する +// 必要があるので分けてある。wxapp.cpp も参照。 bool -parse_logopt(const char *arg, u_int vmtype) +MainApp::Init2() { - char *buf; - char *last; - char *p; - bool rv; - - // "help" (完全一致) なら識別子一覧を表示。 - if (strcmp(arg, "help") == 0) { - for (auto& obj : gObjects) { - if (obj->logname != "?") { - printf("%s\n", obj->logname.c_str()); - } - } - // エイリアスとか - printf("sch -> scheduler\n"); - if (vmtype == VM::TYPE_LUNA) { - printf("ppi -> pio\n"); - printf("scc -> sio\n"); - } - printf("all\n"); + // VM 初期化。 + // これ以降はスレッドを開始したかもしれないので false を返す際には + // VM をデストラクトすること。 + if (!gVM->Init()) { + gVM.reset(); return false; } + // デバッガは VM オブジェクトリストに入っていない + debugger_init(); - rv = false; - buf = strdup(arg); - for (p = strtok_r(buf, ",", &last); - p; - p = strtok_r(NULL, ",", &last)) - { - const char *name; - char *v; - int val; - - name = p; - - v = strchr(p, '='); - if (v) { - *v++ = '\0'; - val = atoi(v); + // 起動時設定の適用 + if (!gVM->Apply()) { + gVM.reset(); + return false; + } + + return true; +} + +// コマンドライン引数を処理する。 +// 知らない引数とかがあればこちらで usage を表示して false を返す。 +bool +MainApp::ParseOpt(int ac, char *av[]) +{ + struct stat st; + const char *cpath; + int b; + int c; + + fontsize = 12; + screen_scale = 1.0; + cpath = "."; + + while ((c = getopt_long(ac, av, "b:B:c:CdDfhHL:M:s:vV:X:", + longopts, NULL)) != -1) { + switch (c) { + case 'b': + debug_breakaddr.push_back(optarg); + break; + + case 'B': + b = atoi(optarg); + if (b < 2 || b > 6) { + fprintf(stderr, "-B : 2..6\n"); + return false; + } + benchmark_mode = b; + break; + + case 'c': + cpath = optarg; + // 空文字列なら再び初期値に + if (cpath[0] == '\0') { + cpath = "."; + } + break; + + case 'C': + log_to_console = true; + break; + + case 'd': + debug_on_start = true; + break; + + case 'D': + debug_on_console = true; + break; + + case 'f': + fast_mode = true; + break; + + case 'H': + human_mode = true; + break; + + case 'L': + if (logopt[0] != '\0') { + strlcat(logopt, ",", sizeof(logopt)); + } + strlcat(logopt, optarg, sizeof(logopt)); + break; + + case 'M': + if (!monitor_opt.empty()) { + monitor_opt += ","; + } + monitor_opt += optarg; + break; + + case 's': + if (IsGUI()) { + char *end; + errno = 0; + screen_scale = strtod(optarg, &end); + if (end == optarg || end[0] != '\0' || errno == ERANGE) { + warnx("-s: invalid argument"); + return false; + } + // 上限は適当 + if (screen_scale <= 0.0 || screen_scale >= 10.0) { + warnx("-s: invalid scale"); + return false; + } + } + break; + + case 'X': + load_and_exec = true; + FALLTHROUGH; + case OPT_load_only: + exec_file = optarg; + for (int i = optind; i < ac; i++) { + if (i != optind) { + exec_arg += " "; + } + exec_arg += av[i]; + } + optind = ac; + break; + + case 'v': + ShowVersion(); + exit(0); + + case 'V': + config_options.push_back(optarg); + break; + + case OPT_fontsize: + fontsize = atoi(optarg); + break; + + case OPT_help: + ShowHelp(true); + return false; + + case OPT_show_config: + show_config = 1; + break; + + case OPT_show_config_all: + show_config = 2; + break; + + case OPT_show_hostnet: + ShowHostnet(); + return false; + + case 'h': + default: + ShowHelp(false); + return false; + } + } + + // 引数がディレクトリなら、それを VM ディレクトリとし、その中の + // nono.cfg を設定ファイルとする。 + // 引数がファイルなら、それを設定ファイルとし、そのファイルがある + // ディレクトリを VM ディレクトリとする。 + // -c DIR => vmdir = DIR, vmfile = DIR/nono.cfg + // -c DIR/FILE => vmdir = DIR, vmfile = FILE + if (stat(cpath, &st) < 0) { + warn("stat %s", cpath); + return false; + } + if (S_ISDIR(st.st_mode)) { + vmdir = std::string(cpath); + if (vmdir.back() != '/') { + vmdir += '/'; + } + vmfile = vmdir + "nono.cfg"; + } else if (S_ISREG(st.st_mode)) { + vmfile = std::string(cpath); + auto pos = vmfile.rfind('/'); + if (pos != std::string::npos) { + vmdir = vmfile.substr(0, pos + 1); } else { - val = 1; + vmdir = "./"; + } + } else { + warnx("-c %s: path must be file or directory", cpath); + return false; + } + + // CLI 版で -Mhelp つけても黙って起動するのはさすがにどうかと思う。 + // とは言え GUI 版のコマンドラインを CLI 版用に書き換えた時に -M を + // 消して回らないと起動できないのも面倒なので -M オプション自体は + // 無意味だけど許容する。うーん。 + if (IsCLI()) { + if (monitor_opt == "help") { + warnx("-Mhelp is not available on CLI"); + return false; + } + if (!monitor_opt.empty()) { + warnx("-M option is ignored on CLI"); + return true; + } + } + + // Human モードでは実行ファイル名が必要 + if (human_mode) { + if (exec_file == NULL) { + warnx("-H option needs -X"); + return false; } - // ここで name は変数名、val は値(省略されたら1) + } + + return true; +} + +// バージョンを表示 +void +MainApp::ShowVersion() const +{ + // ここは実行ファイル名によらず nono にする + fprintf(stderr, "nono version %d.%d.%d (%s)\n", + NONO_MAJOR_VER, NONO_MINOR_VER, NONO_PATCH_VER, NONO_DATE); +} + +// ログレベル指定文字列を処理する。 +// str はログレベル指定文字列を ',' で連結した "foo=1,bar=2" 形式の文字列で、 +// これを分解してそれぞれ担当するオブジェクトのログレベルにセットする。 +// "help" があれば設定は行わず一覧を表示。 +bool +MainApp::ParseLogopt() +{ + std::vector items; - if (strlen(name) < 1) { - printf("invalid logname '%s'\n", name); - goto abort; - } - - // 短縮形とかエイリアスとか - if (strcmp(name, "sch") == 0) - name = "scheduler"; - if (vmtype == VM::TYPE_LUNA) { - if (strcmp(name, "pio") == 0) - name = "ppi"; - if (strcmp(name, "scc") == 0) - name = "sio"; - } - - // 比較 - if (strcmp(name, "all") == 0) { - // "all" なら none 以外の全部にセット - for (auto& obj : gObjects) { - obj->loglevel = val; + // 分解して.. + items = string_split(logopt, ','); + + // "help" があればヘルプを表示して終了 + for (const auto& item : items) { + if (item == "help") { + std::vector list = GetLogNames(); + // less したいだろうから stderr ではなく stdout に出力する + for (const auto& name : list) { + printf(" %s\n", name.c_str()); } - } else { - // それ以外は一致するキーを探してセット - std::string sname = name; - bool found = false; - for (auto& obj : gObjects) { - if (obj->logname == sname) { - obj->loglevel = val; - found = true; - break; - } + return false; + } + } + + // ログレベルを設定 + std::string errmsg; + if (SetLogopt(items, &errmsg) == false) { + warnx("%s", errmsg.c_str()); + return false; + } + + return true; +} + +// ログレベルを設定する。 +// ログレベル指定文字列を 1つずつに分解したリスト items を処理する。 +// "help" があるケースはここに来るまでに処理してあるので、ここには来ない。 +// 成功なら何も表示せず true を返す。失敗なら *errmsg にエラーメッセージを +// 格納して false を返す。 +// MainApp 内と Debugger からも呼ばれる。 +/*static*/ bool +MainApp::SetLogopt(const std::vector& items, std::string *errmsg) +{ + for (const auto& item : items) { + if (SetLogopt1(item.c_str(), errmsg) == false) { + return false; + } + } + if (0) { // デバッグ用 + for (const auto o : gObjects) { + printf("%-16s %d\n", o->GetName().c_str(), o->loglevel); + } + } + return true; +} + +// "logname[=loglevel]" 形式をパースしてオブジェクトにログレベルを設定する。 +// loglevel は省略なら 1 とする。 +// logname が "all" なら全オブジェクトにセットする。 +// そうでない場合は case ignore で完全一致するか前方一致で1つに確定すれば、 +// そのオブジェクトにログレベルを設定して true を返す。 +// 見付からないか候補が複数ある場合はエラーメッセージを *errmsg に出力して +// false を返す。 +// この関数は (このすぐ上の SetLogopt() を経由して) +// MainApp と Debugger から呼ばれることに注意。 +/*static*/ bool +MainApp::SetLogopt1(const std::string& item, std::string *errmsg) +{ + std::string name; + const char *v; + int level; + + v = strchr(item.c_str(), '='); + if (v) { + name = std::string(item.c_str(), v - item.c_str()); + level = atoi(++v); + } else { + name = item; + level = 1; + } + // ここで name は変数名、level は値(省略されたら1) + + if (name.empty()) { + *errmsg = "logname must be specified"; + return false; + } + + // "all" なら全部にセット + if (name == "all") { + for (auto obj : gObjects) { + if (obj->GetName().empty() == false) { + obj->loglevel = level; + } + } + return true; + } + + std::vector found; // 候補オブジェクト + std::vector candidates; // 候補名 + + // 前方一致する名前(とそのオブジェクト)を列挙する + for (auto *obj : gObjects) { + const std::vector& aliases = obj->GetAliases(); + + for (const auto& a : aliases) { + // 完全一致したら即採用 + // (前方一致しつつ完全一致したらこの時点で採用して打ち切るため) + if (strcasecmp(a.c_str(), name.c_str()) == 0) { + obj->loglevel = level; + return true; + } + + // 前方一致したら覚えておく + if (strncasecmp(a.c_str(), name.c_str(), name.size()) == 0) { + candidates.emplace_back(a); + found.push_back(obj); + } + } + } + + // 見付からない場合はエラー + if (found.empty()) { + *errmsg = string_format("Unknown logname \"%s\"", name.c_str()); + return false; + } + + // 1つだけなら確定 + if (found.size() == 1) { + Object *obj = found[0]; + obj->loglevel = level; + return true; + } + + // 複数あれば候補文字列を作成 + *errmsg = string_format("Ambiguous logname \"%s\": candidates are", + name.c_str()); + for (const auto& cand : candidates) { + *errmsg += string_format(" \"%s\"", cand.c_str()); + } + return false; +} + +// ログ名の一覧を取得する。 +// MainApp と Debugger から呼ばれる。 +/*static*/ std::vector +MainApp::GetLogNames() +{ + std::vector sortobj; + + // エイリアスを持つオブジェクトだけ抜き出す + for (const auto& obj : gObjects) { + if (obj->GetAliases().empty() == false) { + sortobj.emplace_back(obj); + } + } + + // aliases の一語目で sortobj をソートする + std::sort(sortobj.begin(), sortobj.end(), + [](const auto& a, const auto *b) { + const auto& sa = a->GetAliases()[0]; + const auto& sb = b->GetAliases()[0]; + return strcmp(sa.c_str(), sb.c_str()) < 0; + } + ); + + std::vector list; + for (const auto *obj : sortobj) { + const auto& aliases = obj->GetAliases(); + std::string str; + + str += aliases[0]; + if (aliases.size() > 1) { + str += " (alias:"; + for (int i = 1, sz = aliases.size(); i < sz; i++) { + str += ' '; + str += aliases[i]; } - // 見つからない場合はエラー - if (!found) { - printf("invalid logname '%s'\n", name); - goto abort; - } - } + str += ')'; + } + list.emplace_back(str); } + list.emplace_back("all"); - rv = true; - abort: - free(buf); - return rv; + return list; +} + +// 関連するファイルのパスを取得。 +// 1. name がファイル名のみなら、VM ディレクトリとその親ディレクトリを検索。 +// ファイルが存在すればその時点で戻り値とする。ファイルが存在したけど +// 何らか不都合があったとしても (例えばファイルサイズが 0 だとか +// パーミッションが足りないとか) 次を試すとかはしない。 +// 2. name が '~' から始まっていればホームディレクトリに展開。 +// 3. name が相対パスなら VM ディレクトリからの相対パスとする。 +// 4. name は絶対パスのはずなので、そのまま使用する。 +std::string +MainApp::SearchFile(const std::string& name) const +{ + std::string path; + struct stat st; + + // 1. パス区切りを含んでなければ、ファイル名のみ + if (name.find('/') == std::string::npos) { + // 1a. VM ディレクトリ + path = GetVMDir() + name; + if (stat(path.c_str(), &st) == 0) { + return path; + } + + // 2. 親ディレクトリ + path = GetVMDir() + "../" + name; + if (stat(path.c_str(), &st) == 0) { + return path; + } + + // どちらもなければエラー + return ""; + } + + // 2. 先頭の '~' を $HOME に展開 + path = name; + if (path[0] == '~' && path[1] == '/') { + const char *home = getenv("HOME"); + if (home == NULL) { + home = ""; + } + path = string_format("%s%s", home, path.c_str() + 1); + } + + // 3. 相対パスなら、VM ディレクトリからの相対 + if (path[0] != '/') { + path = GetVMDir() + path; + } + + return path; +} + +// 現在の VM が指定の feature を持っているか? +bool +MainApp::HasVMFeature(vmf_t feature) const +{ + uint32 vmf = 1 << GetVMType(); + return (vmf & feature) != 0; +} + +// コンパイルされている host netdriver の一覧を表示。 +void +MainApp::ShowHostnet() const +{ + auto list = NetDriverSelector::GetDrivers(); + for (const auto& name : list) { + printf(" %s\n", name.c_str()); + } }