|
|
1.1 root 1: //
2: // nono
1.1.1.2 root 3: // Copyright (C) 2020 nono project
4: // Licensed under nono-license.txt
1.1 root 5: //
6:
1.1.1.10 root 7: //
8: // 設定
9: //
10:
1.1 root 11: #include "config.h"
1.1.1.11 root 12: #include "fdc.h"
1.1 root 13: #include "mystring.h"
14:
1.1.1.10 root 15: // グローバル参照用
16: Config *gConfig;
1.1 root 17:
18: // コンストラクタ
19: Config::Config()
20: {
21: // 設定項目はすべてここで用意すること。
22: // ここで追加した順に表示されるので基本的にはアルファベット順に並べる
23: // (先頭のドットは考慮しなくていい)。
24: // ただし vmtype だけは特別なので最初に表示されたほうがいいだろう。
25:
26: Add("vmtype", "");
27:
28: // 以下概ねアルファベット順に並べる
29:
1.1.1.11 root 30: // X68030 CGROM ファイルパス。なければ内蔵 CGROM。
31: Add("cgrom-image", "", VMCap::X68030);
32:
1.1.1.7 root 33: // VM 内の時計を同期する時間軸。
34: // [ real, virtual ]
35: Add("clock-sync", "real");
36:
1.1.1.10 root 37: // デバッガコンソール
38: Add("debugger-driver", "none");
39: Add("debugger-tcp-port", 0);
40: // デバッガコンソールの fallback は常に不可とする。hostcom.cpp 参照。
41: // "debugger-fallback"
1.1.1.7 root 42:
1.1.1.12 root 43: // DIPSW エイリアス
44: Add("dipsw-autoboot", "", VMCap::LUNA | VMCap::NEWS);
45: Add("dipsw-serial", "", VMCap::LUNA | VMCap::NEWS);
46:
1.1.1.7 root 47: // MAC アドレス
1.1.1.13! root 48: Add("ethernet0-macaddr", "auto");
! 49: Add("ethernet1-macaddr", "auto");
! 50: AddAlias("ethernet-macaddr", "ethernet0-macaddr");
1.1.1.12 root 51:
52: // X68030 の拡張メモリサイズ
53: Add("extram-size", "0", VMCap::X68030);
1.1.1.7 root 54:
1.1.1.11 root 55: // フロッピーディスク
56: Add("fd-drive", 2, VMCap::X68030);
57: for (int id = 0; id < FDCDevice::MAX_DRIVE; id++) {
58: // 種別とイメージパス
59: Add(string_format("fd%d-image", id), VMCap::X68030);
60: // エミュレータ的に書き込みを禁止するか
61: Add(string_format("fd%d-writeignore", id), 0, VMCap::X68030);
62: }
63:
64: // X68030 時の FPU の有無。
65: Add("fpu-type", "68881", VMCap::X68030);
1.1.1.9 root 66:
1.1.1.12 root 67: // 高速モード。
68: Add("fast-mode", "0");
69:
1.1.1.10 root 70: // シリアルポートのホスト側設定
71: Add("hostcom-driver", "none");
72: Add("hostcom-fallback", 0);
73: Add("hostcom-tcp-port", 9998);
74:
1.1.1.11 root 75: // キーボードの入力モード
76: Add("hostkbd-input", "char");
77:
1.1.1.7 root 78: // ネットワークのホスト側設定。
1.1.1.13! root 79: for (int i = 0; i < 2; i++) {
! 80: // 2枚目はデフォルトで none のほうがいいだろう。
! 81: // --show-config での表示順のためループ内におく。
! 82: if (i == 0) {
! 83: Add("hostnet0-driver", "auto");
! 84: } else {
! 85: Add("hostnet1-driver", "none");
! 86: }
! 87: Add(string_format("hostnet%d-fallback", i), 0);
! 88: Add(string_format("hostnet%d-afpacket-ifname", i), "auto");
! 89: Add(string_format("hostnet%d-bpf-ifname", i), "auto");
! 90: Add(string_format("hostnet%d-tap-devpath", i), "auto");
! 91: }
! 92: AddAlias("hostnet-driver", "hostnet0-driver");
! 93: AddAlias("hostnet-fallback", "hostnet0-fallback");
! 94: AddAlias("hostnet-afpacket-ifname", "hostnet0-afpacket-ifname");
! 95: AddAlias("hostnet-bpf-ifname", "hostnet0-bpf-ifname");
! 96: AddAlias("hostnet-tap-devpath", "hostnet0-tap-devpath");
1.1.1.7 root 97:
1.1.1.11 root 98: // IPLROM1/2 ファイルパス。なければ内蔵 ROM。
99: Add("iplrom1-image", "", VMCap::X68030);
100: Add("iplrom2-image", "", VMCap::X68030);
101:
1.1.1.10 root 102: // キーボードの接続
103: Add("keyboard-connect", 1);
104:
1.1.1.8 root 105: // LUNA の RTC(MK48T02) がうるう年でない 1970 をエポックとして
106: // 設定しているので、そちらに合わせてうるう年を補正する
1.1.1.11 root 107: Add("luna-adjust-misused-epoch", 1, VMCap::LUNA);
108:
109: // LUNA の DIPSW (1 が UP、0 が DOWN)。
110: // 初期値は機種によって異なるため pio.cpp で設定している。
111: Add("luna-dipsw1", VMCap::LUNA);
112: Add("luna-dipsw2", VMCap::LUNA);
1.1.1.8 root 113:
1.1.1.10 root 114: // LUNA ビデオボードプレーン数
1.1.1.12 root 115: Add("luna-video-plane", 4, VMCap::LUNA);
1.1.1.10 root 116:
1.1.1.8 root 117: // m88100 の別名ニーモニックを併記する。
1.1.1.11 root 118: Add(".m88k-altname", 0, VMCap::M88K);
1.1.1.8 root 119:
1.1.1.12 root 120: // 画面サイズ。
121: Add("mainview-scale", "1");
122:
123: // モニタウィンドウ(等)のフォントサイズ
124: Add("monitor-fontsize", 12);
125:
1.1.1.7 root 126: // モニタウィンドウの更新頻度 [Hz]
1.1.1.3 root 127: Add("monitor-rate", 20);
1.1.1.7 root 128:
129: // MPU クロック (MHz 単位の実数、小数点以下は3桁まで)。
130: // 初期値は各 VM クラスで設定している。
1.1 root 131: Add("mpu-clock");
1.1.1.7 root 132:
133: // M88100 で疑似 STOP 状態をサポートする
1.1.1.11 root 134: Add("mpu-pseudo-stop", 1, VMCap::M88K);
1.1.1.7 root 135:
1.1.1.13! root 136: // Nereid
! 137: Add("nereid0-enable", 0, VMCap::X68030);
! 138: Add("nereid1-enable", 0, VMCap::X68030);
! 139:
! 140: // Nereid イーサネット
! 141: Add("nereid0-net", 1, VMCap::X68030);
! 142: Add("nereid1-net", 1, VMCap::X68030);
! 143:
! 144: // Nereid バンクメモリ容量 (0, 4, 16 もしくは -4, -16)
! 145: Add("nereid0-ram-size", 16, VMCap::X68030);
! 146: Add("nereid1-ram-size", 16, VMCap::X68030);
! 147:
1.1.1.12 root 148: // NEWS の DIPSW (1 がオン、0 がオフ)。
149: Add("news-dipsw", "00001000", VMCap::NEWS);
150:
151: // NEWS の SIC を無視する。(暫定)
1.1.1.13! root 152: // これは新旧で処置が変わるのでエイリアスにしていない。
1.1.1.12 root 153: Add("xxx-news-sic-ignore", "0", VMCap::NEWS);
154: Add("xxx-news-sci-ignore", "", VMCap::NEWS);
155:
1.1.1.7 root 156: // PROM ファイルパス。なければ内蔵 ROM 起動。
1.1.1.11 root 157: Add("prom-image", "", VMCap::LUNA);
1.1.1.7 root 158:
159: // PROM のデバッグオプション。
160: // PROM 中の SCSI デバッグビットを立てる。
1.1.1.11 root 161: Add(".prom-scsi-debug", 0, VMCap::LUNA1);
1.1.1.7 root 162:
163: // RAM サイズ (MB 単位の整数)。
164: // 初期値は機種ごとの VM クラスで設定している。
1.1.1.2 root 165: Add("ram-size", 0);
1.1.1.7 root 166:
167: // RTC のデバッグ用オプション。
1.1 root 168: Add(".rtc-force-fixed", 0);
1.1.1.8 root 169: Add(".rtc-date");
170: Add(".rtc-time");
1.1.1.7 root 171:
1.1.1.10 root 172: // RTC の epoch。初期値は各 VM クラスで設定している。
1.1.1.12 root 173: Add("rtc-epoch-year", VMCap::LUNA | VMCap::NEWS);
1.1.1.10 root 174:
1.1.1.7 root 175: // ステータスパネルを表示する。
1.1.1.6 root 176: Add("show-statuspanel", 1);
1.1.1.7 root 177:
178: // SCSI 設定。
1.1 root 179: for (int id = 0; id < 8; id++) {
1.1.1.7 root 180: // 種別とイメージパス
1.1 root 181: Add(string_format("spc0-id%d-image", id));
1.1.1.7 root 182: // エミュレータ的に書き込みを禁止するか
1.1.1.8 root 183: Add(string_format("spc0-id%d-writeignore", id), 0);
184: // エミュレータ的に書き込みを禁止するか
185: // (後方互換、十分不要になった頃に削除する)
1.1 root 186: Add(string_format("spc0-id%d-writeprotect", id), 0);
1.1.1.7 root 187: // シークタイム [msec]
1.1.1.4 root 188: Add(string_format("spc0-id%d-seektime", id), 0);
1.1 root 189: }
1.1.1.11 root 190:
191: // SRAM の RAM 容量を同期するか。
192: Add("sram-sync-ramsize", 1, VMCap::X68030);
1.1 root 193: }
194:
195: // デストラクタ
196: Config::~Config()
197: {
1.1.1.10 root 198: gConfig = NULL;
1.1 root 199: }
200:
201: // 項目を追加 (初期状態用)。値は空文字列。
202: bool
1.1.1.11 root 203: Config::Add(const std::string& key, VMCap vmcap)
1.1 root 204: {
1.1.1.11 root 205: return Add(key, "", vmcap);
1.1 root 206: }
207:
208: // 項目を追加 (初期状態用)。値は整数値を文字列にしたもの。
209: bool
1.1.1.11 root 210: Config::Add(const std::string& key, int value, VMCap vmcap)
1.1 root 211: {
1.1.1.11 root 212: return Add(key, std::to_string(value), vmcap);
1.1 root 213: }
214:
215: // 項目を追加 (初期状態用)
216: bool
1.1.1.11 root 217: Config::Add(const std::string& key, const std::string& value, VMCap vmcap)
1.1 root 218: {
219: assert(FindItem(key) == NULL);
1.1.1.2 root 220: assert(fixed == false);
1.1 root 221:
1.1.1.11 root 222: list.emplace_back(key, value, vmcap, ConfigItem::FromInitial, "");
1.1 root 223: return true;
224: }
225:
1.1.1.13! root 226: // エイリアスを追加
! 227: bool
! 228: Config::AddAlias(const std::string& alias, const std::string& key)
! 229: {
! 230: // すでに同名のエイリアスがあれば、プログラムミス
! 231: for (const auto& p : aliases) {
! 232: if (alias == p.first) {
! 233: PANIC("aliases already has %s !", alias.c_str());
! 234: }
! 235: }
! 236:
! 237: aliases.emplace_back(alias, key);
! 238: return true;
! 239: }
! 240:
1.1.1.4 root 241: // 項目を削除
242: // key が見付からなくても何もしない。key は複数登録されていることはないはず。
243: void
244: Config::Delete(const std::string& key)
245: {
246: for (auto it = list.begin(); it != list.end(); ++it) {
247: auto item = *it;
248: if (key == item.key) {
249: list.erase(it);
250: return;
251: }
252: }
253: assert(FindItem(key) == NULL);
254: }
255:
1.1 root 256: // 設定ファイルの内容で上書きする
257: bool
258: Config::Update(const ConfigFile& file)
259: {
1.1.1.2 root 260: assert(fixed == false);
261:
1.1 root 262: for (const auto& pair : file.lines) {
263: const std::string& line = pair.first;
264: int lineno = pair.second;
265:
266: auto where = string_format("%s:%d", file.filepath.c_str(), lineno);
267: if (UpdateLine(line, ConfigItem::FromConfig, where) == false) {
268: return false;
269: }
270: }
271:
272: return true;
273: }
274:
275: // コマンドラインオプションで更新
276: bool
1.1.1.12 root 277: Config::Update(const std::string& line, const std::string& optname)
1.1 root 278: {
1.1.1.2 root 279: assert(fixed == false);
280:
1.1.1.12 root 281: return UpdateLine(line, ConfigItem::FromOption, optname);
1.1 root 282: }
283:
284: // 更新1行分の共通処理。
1.1.1.10 root 285: // キーが見付からない場合、コマンドラインからならエラーとし、
286: // それ以外からの場合は警告のみで無視する。
1.1 root 287: // エラーが起きたらエラーメッセージを表示して false を返す。
288: bool
289: Config::UpdateLine(const std::string& line, ConfigItem::from_t from,
290: const std::string& where)
291: {
292: std::string key;
293: std::string val;
294:
295: // キーと値に分解
296: if (SplitKeyVal(&key, &val, line) == false) {
1.1.1.12 root 297: ConfigItem::PrintErr(from, where, "", line, "Syntax error");
1.1 root 298: return false;
299: }
300:
1.1.1.13! root 301: // キーが別名なら正規名に置換
! 302: key = GetCanonKey(key);
! 303:
1.1.1.10 root 304: // キーで検索
1.1 root 305: ConfigItem *itemptr = FindItem(key);
306: if (itemptr == NULL) {
1.1.1.10 root 307: // コマンドラインからの場合はエラーにする
308: if (from == ConfigItem::FromOption) {
1.1.1.12 root 309: ConfigItem::PrintErr(from, where, key, val, "Unknown key");
1.1.1.10 root 310: return false;
311: }
312: // コマンドライン以外の場合は警告のみで無視する
1.1.1.12 root 313: ConfigItem::PrintErr(from, where, key, val, "Unknown key, ignored");
1.1 root 314: return true;
315: }
316:
317: // 見付かったので更新
318: ConfigItem& item = *itemptr;
319: item.value = val;
320: item.from = from;
321: item.where = where;
322:
323: return true;
324: }
325:
326: // "key=val" 形式の行 line を分解するして *keyp, *valp に代入する。
327: // その際 key, val 前後の空白は取り除く。
328: // 失敗すれば false を返す。
329: /*static*/ bool
330: Config::SplitKeyVal(std::string *keyp, std::string *valp,
331: const std::string& line)
332: {
333: assert(keyp);
334: assert(valp);
335:
336: // キーと値に分解
337: auto pos = line.find('=');
338: if (pos == std::string::npos) {
339: return false;
340: }
341: auto key = line.substr(0, pos++);
342: auto val = line.substr(pos, line.size() - pos);
343:
1.1.1.8 root 344: *keyp = string_trim(key);
345: *valp = string_trim(val);
1.1 root 346:
347: return true;
348: }
349:
1.1.1.13! root 350: // キーがエイリアスなら正規名を返す。
! 351: // エイリアスでなければキーをそのまま返す。
! 352: const std::string&
! 353: Config::GetCanonKey(const std::string& key) const
! 354: {
! 355: for (const auto& p : aliases) {
! 356: const auto& alias = p.first;
! 357: const auto& canon = p.second;
! 358:
! 359: if (key == alias) {
! 360: return canon;
! 361: }
! 362: }
! 363:
! 364: return key;
! 365: }
! 366:
1.1.1.2 root 367: // 内容を表示する。
368: // all なら隠し設定も含めて表示。
1.1 root 369: void
1.1.1.2 root 370: Config::Show(bool all) const
1.1 root 371: {
1.1.1.2 root 372: assert(fixed);
373:
1.1 root 374: for (const auto& item : list) {
1.1.1.2 root 375: // '!' から始まるのは内部用変数なので隠す
376: if (!all && item.key[0] == '!') {
377: continue;
378: }
1.1 root 379: // ドットから始まるのは隠し設定で、初期値のままなら隠す
1.1.1.2 root 380: if (!all &&
381: item.key[0] == '.' && item.from == ConfigItem::FromInitial) {
1.1 root 382: continue;
383: }
384:
385: std::string buf = string_format("%s=\"%s\"",
386: item.key.c_str(), item.value.c_str());
387: if (buf.size() < 40) {
388: int n = ((40 - buf.size()) + 7) / 8;
389: buf.append(n, '\t');
390: } else {
391: buf.push_back('\t');
392: }
393:
394: std::string fromstr;
395: switch (item.from) {
396: case ConfigItem::FromInitial:
397: fromstr = "initial value";
398: break;
399: case ConfigItem::FromConfig:
400: fromstr = item.where;
401: break;
402: case ConfigItem::FromOption:
1.1.1.12 root 403: fromstr = "option " + item.where;
1.1 root 404: break;
405: default:
1.1.1.12 root 406: fromstr = string_format("corrupted item.from=%d", (int)item.from);
407: break;
1.1 root 408: }
409:
410: printf("%s(%s)\n", buf.c_str(), fromstr.c_str());
411: }
412: }
413:
414: // key から ConfigItem を取得する。見付からなければ assert する。
415: const ConfigItem&
1.1.1.2 root 416: Config::Find(const std::string& key) const
1.1 root 417: {
418: for (const auto& item : list) {
419: if (key == item.key) {
420: return item;
421: }
422: }
1.1.1.10 root 423: PANIC("Config::Find() \"%s\" not found", key.c_str());
1.1.1.2 root 424: }
425:
426: // key のデフォルト値を差し替える。key が見付からなければ assert する。
427: void
428: Config::SetDefault(const std::string& key, int val)
429: {
430: std::string valstr = std::to_string(val);
431: SetDefault(key, valstr);
1.1 root 432: }
433:
1.1.1.2 root 434: // key のデフォルト値を差し替える。key が見付からなければ assert する。
435: void
436: Config::SetDefault(const std::string& key, const std::string& val)
1.1 root 437: {
1.1.1.2 root 438: assert(fixed == false);
439:
1.1 root 440: for (auto& item : list) {
441: if (key == item.key) {
1.1.1.2 root 442: item.SetDefault(val);
443: return;
1.1 root 444: }
445: }
1.1.1.2 root 446: assertmsg(0, "Config::SetDefault() \"%s\" not found\n", key.c_str());
1.1 root 447: }
448:
449: // key から ConfigItem を返す。見付からなければ NULL を返す。
450: // (こっちは内部で変更するために使う)
451: ConfigItem *
452: Config::FindItem(const std::string& key)
453: {
454: for (auto& item : list) {
455: if (key == item.key) {
456: return &item;
457: }
458: }
459: return NULL;
460: }
461:
1.1.1.12 root 462: // これ以降は書き込み禁止。
463: bool
1.1.1.2 root 464: Config::Fix()
465: {
466: assert(fixed == false);
467:
1.1.1.11 root 468: // ここで、この機種に不要な変数を削除する。
1.1.1.12 root 469: // が、その前にその変数がユーザから指定されていればエラーにする。
1.1.1.11 root 470: for (auto it = list.begin(); it != list.end(); ) {
471: ConfigItem& item = *it;
472: VMCap vmcap = item.GetVMCap();
473: if (gMainApp.Has(vmcap) == false) {
1.1.1.12 root 474: // この機種用ではない
475:
476: if (item.from != ConfigItem::FromInitial) {
477: // 更新されていればエラー
478: item.Err("Not supported in this vmtype");
479: return false;
480: }
481:
1.1.1.11 root 482: it = list.erase(it);
483: } else {
484: ++it;
485: }
486: }
487:
1.1.1.2 root 488: fixed = true;
1.1.1.12 root 489: return true;
1.1.1.2 root 490: }
491:
1.1 root 492:
493: //
494: // 設定項目
495: //
496:
1.1.1.10 root 497: // 値を整数値として取得してみる。
498: bool
499: ConfigItem::TryInt(int *val) const
500: {
501: char *endp;
502:
503: if (value.empty()) {
504: return false;
505: }
506:
507: errno = 0;
508: int r = strtol(value.c_str(), &endp, 10);
509: if (errno == ERANGE || *endp != '\0') {
510: return false;
511: }
512: *val = r;
513: return true;
514: }
515:
1.1.1.12 root 516: // 値を double 値として取得してみる。
517: bool
518: ConfigItem::TryDouble(double *val) const
519: {
520: char *endp;
521:
522: if (value.empty()) {
523: return false;
524: }
525:
526: errno = 0;
527: double res = strtod(value.c_str(), &endp);
528: if (errno) {
529: return false;
530: }
531: *val = res;
532: return true;
533: }
534:
1.1 root 535: // デフォルト値を差し替える
536: void
537: ConfigItem::SetDefault(const std::string& val)
538: {
539: if (from == FromInitial) {
540: value = val;
541: }
542: }
543:
544: void
545: ConfigItem::Err() const
546: {
547: // 定型文
1.1.1.12 root 548: Err("Invalid argument");
1.1 root 549: }
550:
551: void
552: ConfigItem::Err(const char *fmt, ...) const
553: {
554: char buf[1024];
555: va_list ap;
556:
557: va_start(ap, fmt);
558: vsnprintf(buf, sizeof(buf), fmt, ap);
559: va_end(ap);
560:
561: std::string msg(buf);
562: Err(msg);
563: }
564:
565: void
566: ConfigItem::Err(const std::string& msg) const
567: {
1.1.1.10 root 568: PrintErr(from, where, key, value, msg);
569: }
570:
1.1.1.12 root 571: // この item についてエラーを表示する。
572: // key=val の '=' がない場合のみ key を空、value を行全体として呼ぶ。
1.1.1.10 root 573: /*static*/ void
574: ConfigItem::PrintErr(ConfigItem::from_t from, const std::string& where,
575: const std::string& key, const std::string& value, const std::string& msg)
576: {
1.1 root 577: std::string fromstr;
578:
579: switch (from) {
580: case FromInitial:
1.1.1.12 root 581: // 初期値の場合 key が空なことは起きないはず。
582: // というか初期値でエラーが起きることも普通はないはず。
583: fromstr = "initial value: " + key + "=" + value;
1.1 root 584: break;
1.1.1.12 root 585:
1.1 root 586: case FromConfig:
1.1.1.12 root 587: // 設定ファイルでのエラー。where はファイル名と行番号。
588: fromstr = where + ": ";
589: if (key.empty() == false) {
590: fromstr += key;
1.1.1.13! root 591: fromstr += "=";
1.1.1.12 root 592: }
593: fromstr += value;
1.1 root 594: break;
1.1.1.12 root 595:
1.1 root 596: case FromOption:
1.1.1.12 root 597: // コマンドラインオプションの場合は where は
598: // "--fontsize" とか "-V" なので、
599: // "-V" なら (key =) value を追加し、
600: // それ以外なら value のみ追加がいいか。
601: fromstr = "option " + where + " ";
602: if (where == "-V") {
603: if (key.empty() == false) {
604: fromstr += key;
605: fromstr += "=";
606: }
607: }
608: fromstr += value;
1.1 root 609: break;
1.1.1.10 root 610:
1.1.1.12 root 611: default:
612: fromstr = string_format("corrupted item.from=%d", (int)from);
613: break;
1.1.1.10 root 614: }
615:
1.1.1.12 root 616: warnx("%s: %s", fromstr.c_str(), msg.c_str());
1.1 root 617: }
618:
619:
620: //
621: // 設定ファイル
622: //
623:
624: // コンストラクタ
625: ConfigFile::ConfigFile(const std::string& filepath_)
626: {
627: filepath = filepath_;
628: }
629:
630: // デストラクタ
631: ConfigFile::~ConfigFile()
632: {
633: }
634:
1.1.1.12 root 635: // ファイルの読み込み。
636: // 成功すれば true を、失敗すればメッセージを出力して false を返す。
637: // ファイルがないのは正常とする (なければスルーするため)。
638: // ファイルがあって読めなければ失敗 (エラー終了したいので)。
1.1 root 639: bool
640: ConfigFile::Load()
641: {
642: char buf[1024];
643: FILE *fp;
644:
645: fp = fopen(filepath.c_str(), "r");
646: if (fp == NULL) {
1.1.1.12 root 647: if (errno == ENOENT) {
648: return true;
649: }
1.1 root 650: warn("%s", filepath.c_str());
651: return false;
652: }
653:
654: for (int line = 1; fgets(buf, sizeof(buf), fp) != NULL; line++) {
655: // 空行とコメント行を無視
656: rtrim(buf);
657: if (buf[0] == '#') {
658: buf[0] = '\0';
659: }
660: if (buf[0] == '\0') {
661: continue;
662: }
663:
664: // ここでは記憶するだけ
665: lines.emplace_back(buf, line);
666: }
667:
668: fclose(fp);
669: return true;
670: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.