--- nono/lib/config.cpp 2026/04/29 17:05:01 1.1.1.9 +++ nono/lib/config.cpp 2026/04/29 17:05:39 1.1.1.18 @@ -4,14 +4,16 @@ // Licensed under nono-license.txt // +// +// 設定 +// + #include "config.h" +#include "fdc.h" #include "mystring.h" -std::unique_ptr gConfig; - -// -// 設定クラス -// +// グローバル参照用 +Config *gConfig; // コンストラクタ Config::Config() @@ -25,37 +27,138 @@ Config::Config() // 以下概ねアルファベット順に並べる + // X68030 CGROM ファイルパス。なければ内蔵 CGROM。 + Add("cgrom-image", "", VMCap::X68030); + // VM 内の時計を同期する時間軸。 - // [ real, virtual ] - Add("clock-sync", "real"); + // "real" か "virtual"。初期値は各 VM クラスで設定している。 + Add("clock-sync"); - // デバッガの TCP 待ち受けポート - Add("debugger-port", 0); + // デバッガコンソール + Add("debugger-driver", "none"); + Add("debugger-tcp-port", 0); + // デバッガコンソールの fallback は常に不可とする。hostcom.cpp 参照。 + // "debugger-fallback" + + // DIPSW エイリアス + Add("dipsw-autoboot", "", VMCap::LUNA | VMCap::NEWS); + Add("dipsw-serial", "", VMCap::LUNA | VMCap::NEWS); // MAC アドレス - Add("ethernet-macaddr", "auto"); + Add("ethernet0-macaddr", "auto"); + Add("ethernet1-macaddr", "auto", VMCap::X68030); + AddAlias("ethernet-macaddr", "ethernet0-macaddr"); + + // 実行ファイル(-X) + Add("exec-file"); + Add("exec-arg", "", VMCap::LUNA | VMCap::X68030); + // RAMDISK (--initrd) + Add("exec-initrd", "", VMCap::VIRT68K); + + // virt-m68k のブートパラメータ文字列。BootInfo の BI_COMMANDLINE で、 + // NetBSD/virt68k ではルートデバイスの決定に使っている。 + Add("exec-bootparam", "", VMCap::VIRT68K); + + // X68030 の拡張メモリサイズ + Add("extram-size", "0", VMCap::X68030); + + // フロッピーディスク + Add("fd-drive", 2, VMCap::X68030); + for (int id = 0; id < FDCDevice::MAX_DRIVE; id++) { + // 種別とイメージパス + Add(string_format("fd%u-image", id), VMCap::X68030); + // エミュレータ的に書き込みを禁止するか + Add(string_format("fd%u-writeignore", id), 0, VMCap::X68030); + } + + // 68030 の FPU の有無。 + Add("fpu-type", "68881", VMCap::VIRT68K | VMCap::X68030); + + // 高速モード。 + Add("fast-mode", "0"); + + // ホスト CPU アクセラレーション。 + Add("host-avx2", "auto"); + Add("host-neon", "auto"); + + // CPU アフィニティ (解釈が複雑すぎるので隠しオプション)。 + // "" … 指定しない + // auto … 予約。現状は "" と同じ。 + // low: … Light スレッドを の CPU に割り付ける。 + // high: … Heavy スレッドを の CPU に割り付ける。 + // perf: … Heavy スレッドを の CPU に、 + // Light スレッドをその反集合の CPU に割り付ける。 + Add(".host-cpu-affinity", "auto"); + + // シリアルポートのホスト側設定 + Add("hostcom-driver", "none"); + Add("hostcom-fallback", 0); + Add("hostcom-tcp-port", 9998); // キーボードの入力モード - Add("hostkbd-input", "char"); + Add("hostkbd-input", "char", VMCap::LUNA | VMCap::X68030); // ネットワークのホスト側設定。 - Add("hostnet-driver", "auto"); - Add("hostnet-fallback", 0); - Add("hostnet-afpacket-ifname", "auto"); - Add("hostnet-bpf-ifname", "auto"); - Add("hostnet-tap-devpath", "auto"); + // (0 と 1 で -driver の初期値が違う) + Add("hostnet0-driver", "auto"); + Add("hostnet0-fallback", 0); + Add("hostnet0-afpacket-ifname", "auto"); + Add("hostnet0-bpf-ifname", "auto"); + Add("hostnet0-tap-devpath", "auto"); + Add("hostnet0-usermode-hostfwd",""); + Add("hostnet0-usermode-net", "10.8.0.0/24"); + Add("hostnet0-usermode-net6", "fd08::/64"); + + Add("hostnet1-driver", "none", VMCap::X68030); + Add("hostnet1-fallback", 0, VMCap::X68030); + Add("hostnet1-afpacket-ifname", "auto", VMCap::X68030); + Add("hostnet1-bpf-ifname", "auto", VMCap::X68030); + Add("hostnet1-tap-devpath", "auto", VMCap::X68030); + Add("hostnet1-usermode-hostfwd","", VMCap::X68030); + Add("hostnet1-usermode-net", "10.8.0.0/24", VMCap::X68030); + Add("hostnet1-usermode-net6", "fd08::/64", VMCap::X68030); + + AddAlias("hostnet-driver", "hostnet0-driver"); + AddAlias("hostnet-fallback", "hostnet0-fallback"); + AddAlias("hostnet-afpacket-ifname", "hostnet0-afpacket-ifname"); + AddAlias("hostnet-bpf-ifname", "hostnet0-bpf-ifname"); + AddAlias("hostnet-tap-devpath", "hostnet0-tap-devpath"); + AddAlias("hostnet-usermode-hostfwd","hostnet0-usermode-hostfwd"); + AddAlias("hostnet-usermode-net", "hostnet0-usermode-net"); + AddAlias("hostnet-usermode-net6", "hostnet0-usermode-net6"); + + // IPLROM1/2 ファイルパス。なければ内蔵 ROM。 + Add("iplrom1-image", "", VMCap::X68030); + Add("iplrom2-image", "", VMCap::X68030); - // LUNA の DIPSW (1 が UP、0 が DOWN) - // 初期値は機種によって異なるため pio.cpp で設定している。 - Add("luna-dipsw1"); - Add("luna-dipsw2"); + // キーボードの接続 + Add("keyboard-connect", 1, VMCap::LUNA | VMCap::X68030); // LUNA の RTC(MK48T02) がうるう年でない 1970 をエポックとして // 設定しているので、そちらに合わせてうるう年を補正する - Add("luna-adjust-misused-epoch", 1); + Add("luna-adjust-misused-epoch", 1, VMCap::LUNA); + + // LUNA の DIPSW (1 が UP、0 が DOWN)。 + // 初期値は機種によって異なるため pio.cpp で設定している。 + Add("luna-dipsw1", VMCap::LUNA); + Add("luna-dipsw2", VMCap::LUNA); + + // LUNA ビデオボードプレーン数 + Add("luna-video-plane", 4, VMCap::LUNA); // m88100 の別名ニーモニックを併記する。 - Add(".m88k-altname", 0); + Add(".m88k-altname", 0, VMCap::M88K); + + // m88100/88200 のバージョン。 + // 初期値はコンストラクタ側で用意している。 + Add("m88100-version", 0, VMCap::M88K); + Add("m88200-version", 0, VMCap::M88K); + + // 画面サイズ。 + Add("mainview-scale", "1"); + + // モニタウィンドウ(等)のフォントサイズ + Add("monitor-fontsize", 12); // モニタウィンドウの更新頻度 [Hz] Add("monitor-rate", 20); @@ -64,19 +167,38 @@ Config::Config() // 初期値は各 VM クラスで設定している。 Add("mpu-clock"); - // X68030 時の FPU の有無。 - // LUNA-I では VM 初期化時に削除する。 - Add("mpu-has-fpu", 0); - // M88100 で疑似 STOP 状態をサポートする - Add("mpu-pseudo-stop", 1); + Add("mpu-pseudo-stop", 1, VMCap::M88K); + + // MPU 種別。現状 m68k のみ。初期値は機種依存。 + Add("mpu-type", "", VMCap::M68K); + + // Nereid + Add("nereid0-enable", 0, VMCap::X68030); + Add("nereid1-enable", 0, VMCap::X68030); + + // Nereid イーサネット + Add("nereid0-net", 1, VMCap::X68030); + Add("nereid1-net", 1, VMCap::X68030); + + // Nereid バンクメモリ容量 (0, 4, 16 もしくは -4, -16) + Add("nereid0-ram-size", 16, VMCap::X68030); + Add("nereid1-ram-size", 16, VMCap::X68030); + + // NEWS の DIPSW (1 がオン、0 がオフ)。 + Add("news-dipsw", "00001000", VMCap::NEWS); + + // NEWS の SIC を無視する。(暫定) + // これは新旧で処置が変わるのでエイリアスにしていない。 + Add("xxx-news-sic-ignore", "0", VMCap::NEWS); + Add("xxx-news-sci-ignore", "", VMCap::NEWS); // PROM ファイルパス。なければ内蔵 ROM 起動。 - Add("prom-image", ""); + Add("prom-image", "", VMCap::LUNA); // PROM のデバッグオプション。 // PROM 中の SCSI デバッグビットを立てる。 - Add(".prom-scsi-debug", 0); + Add(".prom-scsi-debug", 0, VMCap::LUNA1); // RAM サイズ (MB 単位の整数)。 // 初期値は機種ごとの VM クラスで設定している。 @@ -87,50 +209,94 @@ Config::Config() Add(".rtc-date"); Add(".rtc-time"); + // RTC の epoch。初期値は各 VM クラスで設定している。 + Add("rtc-epoch-year", VMCap::LUNA | VMCap::NEWS); + // ステータスパネルを表示する。 Add("show-statuspanel", 1); // SCSI 設定。 for (int id = 0; id < 8; id++) { + auto scsicap = VMCap::LUNA | VMCap::X68030; // 種別とイメージパス - Add(string_format("spc0-id%d-image", id)); - // エミュレータ的に書き込みを禁止するか - Add(string_format("spc0-id%d-writeignore", id), 0); + Add(string_format("spc0-id%u-image", id), scsicap); // エミュレータ的に書き込みを禁止するか - // (後方互換、十分不要になった頃に削除する) - Add(string_format("spc0-id%d-writeprotect", id), 0); + Add(string_format("spc0-id%u-writeignore", id), 0, scsicap); // シークタイム [msec] - Add(string_format("spc0-id%d-seektime", id), 0); + Add(string_format("spc0-id%u-seektime", id), 0, scsicap); } + + // SRAM の RAM 容量を同期するか。 + Add("sram-sync-ramsize", 1, VMCap::X68030); + + // virtio 設定。 + auto viocap = VMCap::VIRT68K; + for (int id = 0; id < 8; id++) { + // イメージパス + Add(string_format("virtio-block%u-image", id), viocap); + // エミュレータ的に書き込みを禁止するか + Add(string_format("virtio-block%u-writeignore", id), 0, viocap); + } + for (int id = 0; id < 8; id++) { + // 種別とイメージパス + Add(string_format("virtio-scsi-id%u-image", id), viocap); + // エミュレータ的に書き込みを禁止するか + Add(string_format("virtio-scsi-id%u-writeignore", id), 0, viocap); + } + + // Windrv パス。 + Add("windrv-path", "", VMCap::X68030); + + // X68030 で FC2 ピンを切るか。 + Add("x68k-cut-fc2", 1, VMCap::X68030); + + // XP プロセッサのクロック (MHz 単位の実数、小数点以下は3桁まで)。 + Add("xp-clock", "6.144", VMCap::LUNA); } // デストラクタ Config::~Config() { + gConfig = NULL; } // 項目を追加 (初期状態用)。値は空文字列。 bool -Config::Add(const std::string& key) +Config::Add(const std::string& key, VMCap vmcap) { - return Add(key, ""); + return Add(key, "", vmcap); } // 項目を追加 (初期状態用)。値は整数値を文字列にしたもの。 bool -Config::Add(const std::string& key, int value) +Config::Add(const std::string& key, int value, VMCap vmcap) { - return Add(key, std::to_string(value)); + return Add(key, std::to_string(value), vmcap); } // 項目を追加 (初期状態用) bool -Config::Add(const std::string& key, const std::string& value) +Config::Add(const std::string& key, const std::string& value, VMCap vmcap) { assert(FindItem(key) == NULL); assert(fixed == false); - list.emplace_back(key, value, ConfigItem::FromInitial, ""); + list.emplace_back(key, value, vmcap, ConfigItem::FromInitial, ""); + return true; +} + +// エイリアスを追加 +bool +Config::AddAlias(const std::string& alias, const std::string& key) +{ + // すでに同名のエイリアスがあれば、プログラムミス + for (const auto& p : aliases) { + if (alias == p.first) { + PANIC("aliases already has %s !", alias.c_str()); + } + } + + aliases.emplace_back(alias, key); return true; } @@ -157,10 +323,10 @@ Config::Update(const ConfigFile& file) for (const auto& pair : file.lines) { const std::string& line = pair.first; - int lineno = pair.second; + uint lineno = pair.second; - auto where = string_format("%s:%d", file.filepath.c_str(), lineno); - if (UpdateLine(line, ConfigItem::FromConfig, where) == false) { + auto where = string_format("%s:%u", file.filepath.c_str(), lineno); + if (UpdateLine(line, file.from, where) == false) { return false; } } @@ -168,24 +334,61 @@ Config::Update(const ConfigFile& file) return true; } +#define UpdateLinePerf(str) do { \ + if (UpdateLine(str, ConfigItem::FromPerf, where) == false) \ + return false; \ +} while (0) + // コマンドラインオプションで更新 bool -Config::Update(const std::vector& options) +Config::Update(const std::string& line, const std::string& optname) { assert(fixed == false); - for (const auto& line : options) { - if (UpdateLine(line, ConfigItem::FromOption, "") == false) { - return false; - } + // パフォーマンス測定用オプション --perf が指定されたら + // line="--perf" という非正規の書式で呼ばれるので、ここで展開する。 + if (line == "--perf") { + // -V prom-image=PROM.DAT はパスの問題があるので、各自で追加指定 + // する必要がある。 + std::string where = "--perf -V"; + UpdateLinePerf(".rtc-force-fixed=1"); + UpdateLinePerf("clock-sync=virtual"); + UpdateLinePerf("fast-mode=1"); + UpdateLinePerf("fd0-writeignore=1"); + UpdateLinePerf("fd1-writeignore=1"); + UpdateLinePerf("fd2-writeignore=1"); + UpdateLinePerf("fd3-writeignore=1"); + UpdateLinePerf("ethernet-macaddr=02:00:00:00:00:01"); + UpdateLinePerf("hostcom-driver=none"); + UpdateLinePerf("hostnet-driver=none"); + UpdateLinePerf("luna-dipsw1=11110111"); + UpdateLinePerf("luna-video-plane=4"); + UpdateLinePerf("spc0-id0-writeignore=1"); + UpdateLinePerf("spc0-id1-writeignore=1"); + UpdateLinePerf("spc0-id2-writeignore=1"); + UpdateLinePerf("spc0-id3-writeignore=1"); + UpdateLinePerf("spc0-id4-writeignore=1"); + UpdateLinePerf("spc0-id5-writeignore=1"); + UpdateLinePerf("spc0-id6-writeignore=1"); + UpdateLinePerf("virtio-block0-writeignore=1"); + UpdateLinePerf("virtio-block1-writeignore=1"); + UpdateLinePerf("virtio-block2-writeignore=1"); + UpdateLinePerf("virtio-block3-writeignore=1"); + UpdateLinePerf("virtio-block4-writeignore=1"); + UpdateLinePerf("virtio-block5-writeignore=1"); + UpdateLinePerf("virtio-block6-writeignore=1"); + return true; + } else { + return UpdateLine(line, ConfigItem::FromOption, optname); } - - return true; } // 更新1行分の共通処理。 -// キーが見付からないのはエラーとはしない(?)。 -// エラーが起きたらエラーメッセージを表示して false を返す。 +// キーが見付からない場合は from によって動作が異なる。 +// o 共通設定ファイルでなら、何もせず true を返す。 +// o VM 設定ファイルでなら、エラーメッセージを表示するが true を返す。 +// o コマンドラインでなら、エラーメッセージを表示して false を返す。 +// 戻り値 false は実行中止を示す。 bool Config::UpdateLine(const std::string& line, ConfigItem::from_t from, const std::string& where) @@ -195,20 +398,37 @@ Config::UpdateLine(const std::string& li // キーと値に分解 if (SplitKeyVal(&key, &val, line) == false) { - std::string fromstr; - if (from == ConfigItem::FromOption) { - fromstr = "-V " + line; - } else { - fromstr = where; - } - warnx("%s: syntax error", fromstr.c_str()); + ConfigItem::PrintErr(from, where, "", line, "Syntax error"); return false; } - // キーで検索 (なければ無視?) + // キーが別名なら正規名に置換 + key = GetCanonKey(key); + + // キーで検索 ConfigItem *itemptr = FindItem(key); if (itemptr == NULL) { - return true; + switch (from) { + default: + case ConfigItem::FromHome: + case ConfigItem::FromPerf: + // 共通設定ファイルなら黙って無視する。 + // このファイルには他機種向けの設定も並んでいる可能性があるため + // 無視してくれないと困る。 + return true; + + case ConfigItem::FromConfig: + // VM 設定ファイルなら警告のみで無視する。 + // このファイルに他機種向けの設定があるのはおかしいが + // 止めるほどではない。 + ConfigItem::PrintErr(from, where, key, val, "Unknown key, ignored"); + return true; + + case ConfigItem::FromOption: + // コマンドラインからの場合はエラーにする。 + ConfigItem::PrintErr(from, where, key, val, "Unknown key"); + return false; + } } // 見付かったので更新 @@ -244,6 +464,23 @@ Config::SplitKeyVal(std::string *keyp, s return true; } +// キーがエイリアスなら正規名を返す。 +// エイリアスでなければキーをそのまま返す。 +const std::string& +Config::GetCanonKey(const std::string& key) const +{ + for (const auto& p : aliases) { + const auto& alias = p.first; + const auto& canon = p.second; + + if (key == alias) { + return canon; + } + } + + return key; +} + // 内容を表示する。 // all なら隠し設定も含めて表示。 void @@ -265,7 +502,7 @@ Config::Show(bool all) const std::string buf = string_format("%s=\"%s\"", item.key.c_str(), item.value.c_str()); if (buf.size() < 40) { - int n = ((40 - buf.size()) + 7) / 8; + uint n = ((40 - buf.size()) + 7) / 8; buf.append(n, '\t'); } else { buf.push_back('\t'); @@ -276,14 +513,20 @@ Config::Show(bool all) const case ConfigItem::FromInitial: fromstr = "initial value"; break; + case ConfigItem::FromHome: case ConfigItem::FromConfig: fromstr = item.where; break; case ConfigItem::FromOption: - fromstr = "-V option"; + fromstr = "option " + item.where; + break; + case ConfigItem::FromPerf: + fromstr = "option --perf"; break; default: - __unreachable(); + fromstr = string_format("corrupted item.from=%u for key=%s", + (uint)item.from, item.key.c_str()); + break; } printf("%s(%s)\n", buf.c_str(), fromstr.c_str()); @@ -299,7 +542,7 @@ Config::Find(const std::string& key) con return item; } } - assertmsg(0, "Config::Find() \"%s\" not found\n", key.c_str()); + PANIC("Config::Find() \"%s\" not found", key.c_str()); } // key のデフォルト値を差し替える。key が見付からなければ assert する。 @@ -338,13 +581,37 @@ Config::FindItem(const std::string& key) return NULL; } -// これ以降は書き込み禁止 -void +// これ以降は書き込み禁止。 +bool Config::Fix() { assert(fixed == false); + // ここで、この機種に不要な変数を削除する。 + // その変数がユーザから指定されていればエラーにする。 + for (auto it = list.begin(); it != list.end(); ) { + ConfigItem& item = *it; + VMCap vmcap = item.GetVMCap(); + if (gMainApp.Has(vmcap) == false) { + // この機種用ではない + switch (item.from) { + case ConfigItem::FromConfig: + case ConfigItem::FromOption: + // 明示的に更新されていればエラー + item.Err("Not supported in this vmtype"); + return false; + default: + break; + } + + it = list.erase(it); + } else { + ++it; + } + } + fixed = true; + return true; } @@ -352,6 +619,97 @@ Config::Fix() // 設定項目 // +// 値を整数値として取得してみる。 +bool +ConfigItem::TryInt(int *val) const +{ + char *endp; + + if (value.empty()) { + return false; + } + + errno = 0; + int r = strtol(value.c_str(), &endp, 10); + if (errno == ERANGE || *endp != '\0') { + return false; + } + *val = r; + return true; +} + +// 値を double 値として取得してみる。 +bool +ConfigItem::TryDouble(double *val) const +{ + char *endp; + + if (value.empty()) { + return false; + } + + errno = 0; + double res = strtod(value.c_str(), &endp); + if (errno) { + return false; + } + *val = res; + return true; +} + +// b を e 乗した整数を返す。 +static inline int +ipow(int b, int e) +{ + int rv = 1; + + for (int i = 0; i < e; i++) { + rv *= b; + } + return rv; +} + +// 値を固定小数点値として取得してみる。 +// digit は小数点以下の桁数で、それ以下は四捨五入する。 +// 値が "1.2345" で digit=3 なら *val = 1235 を返す。 +bool +ConfigItem::TryFixedDecimal(int *val, int digit) const +{ + char *endp; + bool neg = false; + + if (value.empty()) { + return false; + } + + errno = 0; + int r = strtol(value.c_str(), &endp, 10); + if (errno == ERANGE || !(*endp == '\0' || *endp == '.')) { + return false; + } + if (r < 0) { + r = -r; + neg = true; + } + r *= ipow(10, digit); + if (*endp == '.') { + char *p = endp + 1; + errno = 0; + int f = strtol(p, &endp, 10); + if (errno == ERANGE || *endp != '\0') { + return false; + } + int n = endp - p; + if (n <= digit) { + r += f * ipow(10, digit - n); + } else { + r += f / ipow(10, n - digit) + (p[digit] >= '5'); + } + } + *val = neg ? -r : r; + return true; +} + // デフォルト値を差し替える void ConfigItem::SetDefault(const std::string& val) @@ -365,7 +723,7 @@ void ConfigItem::Err() const { // 定型文 - Err("invalid argument"); + Err("Invalid argument"); } void @@ -385,23 +743,58 @@ ConfigItem::Err(const char *fmt, ...) co void ConfigItem::Err(const std::string& msg) const { + PrintErr(from, where, key, value, msg); +} + +// この item についてエラーを表示する。 +// key=val の '=' がない場合のみ key を空、value を行全体として呼ぶ。 +/*static*/ void +ConfigItem::PrintErr(ConfigItem::from_t from, const std::string& where, + const std::string& key, const std::string& value, const std::string& msg) +{ std::string fromstr; switch (from) { case FromInitial: - fromstr = "initial value:"; + // 初期値の場合 key が空なことは起きないはず。 + // というか初期値でエラーが起きることも普通はないはず。 + fromstr = "initial value: " + key + "=" + value; break; + + case FromHome: case FromConfig: - fromstr = where + ":"; + // 設定ファイルでのエラー。where はファイル名と行番号。 + fromstr = where + ": "; + if (key.empty() == false) { + fromstr += key; + fromstr += "="; + } + fromstr += value; break; + case FromOption: - fromstr = "option -V"; + case FromPerf: + // コマンドラインオプションの場合は where は + // "--fontsize" とか "-V" なので、 + // "-V" なら (key =) value を追加し、 + // それ以外なら value のみ追加がいいか。 + fromstr = "option " + where + " "; + if (where.find("-V") != std::string::npos) { + if (key.empty() == false) { + fromstr += key; + fromstr += "="; + } + } + fromstr += value; break; + default: - __unreachable(); + fromstr = string_format("corrupted item.from=%u for key=%s", + (uint)from, key.c_str()); + break; } - warnx("%s %s=%s: %s", - fromstr.c_str(), key.c_str(), value.c_str(), msg.c_str()); + + warnx("%s: %s", fromstr.c_str(), msg.c_str()); } @@ -410,9 +803,10 @@ ConfigItem::Err(const std::string& msg) // // コンストラクタ -ConfigFile::ConfigFile(const std::string& filepath_) +ConfigFile::ConfigFile(const std::string& filepath_, ConfigItem::from_t from_) { filepath = filepath_; + from = from_; } // デストラクタ @@ -420,15 +814,25 @@ ConfigFile::~ConfigFile() { } -// ファイルの読み込み +// ファイルの読み込み。 +// 成功すれば true を、失敗すればメッセージを出力して false を返す。 +// ファイルがないのは正常とする (なければスルーするため)。 +// ファイルがあって読めなければ失敗 (エラー終了したいので)。 bool ConfigFile::Load() { char buf[1024]; FILE *fp; + if (filepath.empty()) { + return true; + } + fp = fopen(filepath.c_str(), "r"); if (fp == NULL) { + if (errno == ENOENT) { + return true; + } warn("%s", filepath.c_str()); return false; }