--- nono/lib/mainapp.cpp 2026/04/29 17:04:28 1.1.1.1 +++ nono/lib/mainapp.cpp 2026/04/29 17:04:40 1.1.1.5 @@ -1,25 +1,366 @@ // // 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 +#include + +// グローバルインスタンス +MainApp gMainApp; + +// ヘルプメッセージ +void +MainApp::ShowHelp(bool all) const +{ + ShowVersion(); + +#define p(msg) printf(" " msg "\n") + + printf("usage: %s []\n", getprogname()); + p("-A load and execute host binary (a.out or ELF)"); + p("-c vm directory"); + 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("-v show version"); + p("-V = overwrite config option"); + + 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("-L =[,..] set loglevel (-Lhelp displays names)"); + p("-M [,..] monitors to display at startup (-Mhelp displays names)"); + p("-X human68k console emulation"); + } +} + +// long option は enum と struct option をアルファベット順に並べる。 +// enum は getopt() の1文字のオプションと衝突しなければいいので適当に +// 0x80 から始めておく。 +enum { + OPT_fontsize = 0x80, + OPT_help, + OPT_show_config, + OPT_show_config_all, +}; +static struct option longopts[] = { + { "fontsize", required_argument, NULL, OPT_fontsize }, + { "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 }, + { NULL, 0, NULL, 0 }, +}; + +// 起動時の処理、VM の実行開始前まで。 +// 所々 CLI と GUI で処理が違う。 +// 戻り値は以下のいずれか。 +// MainApp::PASS .. 実行を継続(通常パス) +// EXIT_SUCCESS / EXIT_FAILURE .. この終了コードでアプリケーションを終了 +int +MainApp::Init(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(vmdir + "nono.cfg"); + if (file.Load() == false) { + return EXIT_FAILURE; + } + if (gConfig->Update(file) == false) { + return EXIT_FAILURE; + } + // 最後にコマンドライン引数で更新 + if (gConfig->Update(config_options) == false) { + return EXIT_FAILURE; + } + + // VM 種別を決定 + if (IsCLI() && human68k_file) { + vmtype = VMTYPE_RXZ; + } else { + // 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; + } + } + + // VM 作成 + switch (vmtype) { + case VMTYPE_X68030: + gVM.reset(new VM_X68030()); + break; + + case VMTYPE_LUNA1: + gVM.reset(new VM_LUNA()); + break; + + case VMTYPE_RXZ: + gVM.reset(new VM_RXZ()); + break; + + case VMTYPE_LUNA88K: + gVM.reset(new VM_LUNA88K()); + break; + + default: + __unreachable(); + } + + // 動的なコンストラクション + if (!gVM->Create()) { + return EXIT_FAILURE; + } + + // 設定内容を表示して終了 + // (VM コンストラクタで変数の増減があるのでそれより後、 + // Create() でも SCSI パラメータを減らすので、それより後) + gConfig->Fix(); + if (show_config) { + gConfig->Show(show_config - 1); + return EXIT_SUCCESS; + } + + // ログレベルを設定。コンストラクト後すぐに行う。 + // -L help もここで処理。 + if (!ParseLogopt()) { + return EXIT_FAILURE; + } + + return PASS; +} + +// VM の実行部分。 +// Init() で引数を受け付けてから Start() でデバッガスレッドなどを起動するが、 +// -Mhelp とかが指定された場合はここでスレッド開始する前にプロセスを終了する +// 必要があるので、分けてある。 +bool +MainApp::Start() +{ + // VM 初期化。 + // これ以降はスレッドを開始したかもしれないので false を返す際には + // VM をデストラクトすること。 + if (!gVM->Init()) { + gVM.reset(); + return false; + } + // デバッガは VM オブジェクトリストに入っていない + debugger_init(); + + // 起動時設定の適用 + if (!gVM->Apply()) { + gVM.reset(); + return false; + } + + return true; +} + +// コマンドライン引数を処理する。 +// 知らない引数とかがあればこちらで usage を表示して false を返す。 +bool +MainApp::ParseOpt(int ac, char *av[]) +{ + int b; + int c; + + fontsize = 12; + screen_scale = 1.0; + + while ((c = getopt_long(ac, av, "A:b:B:c:CdDfhL:M:s:vV:X:", + longopts, NULL)) != -1) { + switch (c) { + case 'A': + host_file = optarg; + break; + + 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': + vmdir = std::string(optarg); + 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 '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': + human68k_file = optarg; + for (int i = optind; i < ac; i++) { + if (i != optind) { + strlcat(human68k_arg, " ", sizeof(human68k_arg)); + } + strlcat(human68k_arg, av[i], sizeof(human68k_arg)); + } + 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 'h': + default: + ShowHelp(false); + return false; + } + } + + // vmdir に '/' を付けておく + if (vmdir.empty()) { + vmdir = "."; + } + if (vmdir.back() != '/') { + vmdir += '/'; + } + + // 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; + } + } -// 高速モード (XXX 設定ファイルと含めて見直すこと) -bool fast_mode; + return true; +} -// VM ディレクトリ -std::string vmdir; +// バージョンを表示 +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); +} -// ログ指定文字列 (-L) -char logopt[256]; -// 引数 arg のログ指定文字列をパースする。 +// 引数 logopt のログ指定文字列をパースする。 // arg は "foo=1,bar=2" 形式の文字列で、これを分解して // それぞれ担当するオブジェクトのログレベルにセットする。 bool -parse_logopt(const char *arg, u_int vmtype) +MainApp::ParseLogopt() { char *buf; char *last; @@ -27,7 +368,7 @@ parse_logopt(const char *arg, u_int vmty bool rv; // "help" (完全一致) なら識別子一覧を表示。 - if (strcmp(arg, "help") == 0) { + if (strcmp(logopt, "help") == 0) { for (auto& obj : gObjects) { if (obj->logname != "?") { printf("%s\n", obj->logname.c_str()); @@ -35,8 +376,7 @@ parse_logopt(const char *arg, u_int vmty } // エイリアスとか printf("sch -> scheduler\n"); - if (vmtype == VM::TYPE_LUNA) { - printf("ppi -> pio\n"); + if (gMainApp.HasVMFeature(VMF_LUNA)) { printf("scc -> sio\n"); } printf("all\n"); @@ -44,7 +384,7 @@ parse_logopt(const char *arg, u_int vmty } rv = false; - buf = strdup(arg); + buf = strdup(logopt); for (p = strtok_r(buf, ",", &last); p; p = strtok_r(NULL, ",", &last)) @@ -72,9 +412,7 @@ parse_logopt(const char *arg, u_int vmty // 短縮形とかエイリアスとか if (strcmp(name, "sch") == 0) name = "scheduler"; - if (vmtype == VM::TYPE_LUNA) { - if (strcmp(name, "pio") == 0) - name = "ppi"; + if (gMainApp.HasVMFeature(VMF_LUNA)) { if (strcmp(name, "scc") == 0) name = "sio"; } @@ -86,14 +424,14 @@ parse_logopt(const char *arg, u_int vmty obj->loglevel = val; } } else { - // それ以外は一致するキーを探してセット + // それ以外は一致するキーを探してセット。 + // 複数のオブジェクトが同じ logname を持ってもよい (CMMUとか)。 std::string sname = name; bool found = false; for (auto& obj : gObjects) { if (obj->logname == sname) { obj->loglevel = val; found = true; - break; } } // 見つからない場合はエラー @@ -109,3 +447,61 @@ parse_logopt(const char *arg, u_int vmty free(buf); return rv; } + +// 関連するファイルのパスを取得。 +// 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; +}