From 5fba0383131ef4259c37cd5841e78be08474d184 Mon Sep 17 00:00:00 2001 From: vetusmagnus <111835005+vetusmagnus@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:49:56 -0400 Subject: [PATCH] Linux diagnostics updates --- .gitignore | 2 +- README.md | 8 +- games/rac1/build.py | 2 +- games/rac1/host/include/uya_runtime.h | 1 + games/rac1/host/src/main.c | 1 + games/rac1/host/src/platform_linux.c | 22 ++++ games/rac1/host/src/platform_linux_sdl.h | 11 +- games/rac1/host/src/platform_win32.c | 26 ++++ games/rac1/host/src/runtime.c | 5 + launcher/linux/project-solana | 147 +++++++++++++++++++++-- 10 files changed, 211 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index ab043b0..dc4b846 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ games/*/zig-cache/ games/*/build_log.txt __pycache__/ *.pyc -release/rac1/ +release/rac1/ \ No newline at end of file diff --git a/README.md b/README.md index 917773f..33c0cb7 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ Resolution (Native, or Upscaled to a whole multiple of the PS2's 448 lines that ### Reporting problems - **Open log** shows `rac1_log.txt` (or the build's `build_log.txt` when a build failed). +- On Linux, `project-solana report` writes one file with everything a report needs (system details, the 32-bit library check, the launcher, build and game logs, and the previous run's game log); attach that file. +- A crash writes the function it happened in and the call chain into the game log, and the previous run's log is kept as `rac1_log.prev.txt`. - **F9** in the game records the next few seconds of graphics input to `gsrec_NN.bin` (about 260 MB per second of capture); attach it to a report together with what looked wrong. ## Repository layout @@ -86,4 +88,8 @@ gh release create rac1 dist/rac1/rac1.json dist/rac1/rac1-build.zip dist/rac1/to To update an existing game release, upload the new files over the old ones (`gh release upload rac1 dist/rac1/* --clobber`). Players get them with **Rebuild the game**. -The launchers go in a release of their own (for example `launcher-v1`): `Project Solana.exe` and `project-solana` (Linux). \ No newline at end of file +The launchers go in a release of their own (for example `launcher-v1`): `Project Solana.exe` and `project-solana` (Linux). + +## Licence + +TODO (maintainers): choose a licence for this repository. Third-party code keeps its own (see `THIRD_PARTY_NOTICES.md`). diff --git a/games/rac1/build.py b/games/rac1/build.py index c29ef62..99efeb5 100644 --- a/games/rac1/build.py +++ b/games/rac1/build.py @@ -241,7 +241,7 @@ if a.platform == "linux": else: # the menu and runtime as a DLL at 0x40000000, plus the small launcher that owns PS2 RAM dll = os.path.join(out, "rac1_game.dll") - link = zig + ["cc", "-shared", "-target", target, "-o", dll, objs_rsp("link")] + ["-Wl,--image-base=0x40000000", "-Wl,--no-dynamicbase", "-lshell32", "-lwinmm", "-lgdi32", "-luser32"] + link = zig + ["cc", "-shared", "-target", target, "-o", dll, objs_rsp("link")] + ["-Wl,--image-base=0x40000000", "-Wl,--no-dynamicbase", "-lshell32", "-lwinmm", "-lgdi32", "-luser32", "-ladvapi32"] r = subprocess.run(link, capture_output=True, text=True) if r.returncode: print(r.stderr[-6000:]); sys.exit("link failed (dll)") diff --git a/games/rac1/host/include/uya_runtime.h b/games/rac1/host/include/uya_runtime.h index 50750de..7ae7322 100644 --- a/games/rac1/host/include/uya_runtime.h +++ b/games/rac1/host/include/uya_runtime.h @@ -23,6 +23,7 @@ extern const int uya_tramp_count; /* platform layer */ int plat_map_fixed(uint32_t addr, uint32_t size, int exec); /* 0 on success */ int plat_install_trap_handler(void); /* calls into unmapped PS2 code */ +void plat_log_system(void); /* the system, for bug reports (once, at start) */ void plat_flush_icache(void *p, size_t n); const char *plat_exe_dir(void); /* folder of the running program */ void plat_at_exit(void); diff --git a/games/rac1/host/src/main.c b/games/rac1/host/src/main.c index 198f545..a4b46e9 100644 --- a/games/rac1/host/src/main.c +++ b/games/rac1/host/src/main.c @@ -63,6 +63,7 @@ int uya_main(int argc, char **argv) uya_log_open(); uya_log("Project Solana: starting"); plat_install_trap_handler(); /* first: a crash from here on is logged */ + plat_log_system(); { /* renderer=gl|soft in the cfg (UYA_RENDERER overrides it) */ extern const char *uya_renderer_cfg; const char *r = cfg_value("renderer"); diff --git a/games/rac1/host/src/platform_linux.c b/games/rac1/host/src/platform_linux.c index b2120fc..df409d7 100644 --- a/games/rac1/host/src/platform_linux.c +++ b/games/rac1/host/src/platform_linux.c @@ -180,8 +180,30 @@ int plat_install_trap_handler(void) sigaction(SIGBUS, &sa, 0); sigaction(SIGILL, &sa, 0); sigaction(SIGFPE, &sa, 0); + sigaction(SIGABRT, &sa, 0); /* (a library giving up: assert, abort) */ return 0; } + +#include +void plat_log_system(void) +{ + struct utsname u; + char os[160] = "unknown", line[256], cpu[160] = "?"; + FILE *f = fopen("/etc/os-release", "r"); + while (f && fgets(line, sizeof line, f)) + if (!strncmp(line, "PRETTY_NAME=", 12)) { char *v = line + 12; if (*v == '"') v++; v[strcspn(v, "\"\n")] = 0; snprintf(os, sizeof os, "%s", v); } + if (f) fclose(f); + f = fopen("/proc/cpuinfo", "r"); + while (f && fgets(line, sizeof line, f)) + if (!strncmp(line, "model name", 10)) { char *v = strchr(line, ':'); if (v) { v += 2; v[strcspn(v, "\n")] = 0; snprintf(cpu, sizeof cpu, "%s", v); break; } } + if (f) fclose(f); + if (uname(&u) == 0) uya_log("system: Linux %s (%s), %s", u.release, u.machine, os); + uya_log("system: %s, %ld cores, %ld MB RAM", cpu, sysconf(_SC_NPROCESSORS_ONLN), (long)((long long)sysconf(_SC_PHYS_PAGES) * sysconf(_SC_PAGESIZE) >> 20)); + const char *st = getenv("XDG_SESSION_TYPE"), *de = getenv("XDG_CURRENT_DESKTOP"); + uya_log("system: session %s%s%s, DISPLAY %s, WAYLAND_DISPLAY %s%s", st ? st : "?", de ? ", desktop " : "", de ? de : "", + getenv("DISPLAY") ? getenv("DISPLAY") : "-", getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "-", + getenv("SteamDeck") && !strcmp(getenv("SteamDeck"), "1") ? ", Steam Deck" : getenv("SteamAppId") ? ", started by Steam" : ""); +} #endif #ifdef __linux__ diff --git a/games/rac1/host/src/platform_linux_sdl.h b/games/rac1/host/src/platform_linux_sdl.h index f58eb09..573fb90 100644 --- a/games/rac1/host/src/platform_linux_sdl.h +++ b/games/rac1/host/src/platform_linux_sdl.h @@ -83,6 +83,9 @@ static struct { int (*QueueAudio)(SDL_AudioDeviceID, const void *, uint32_t); uint32_t (*GetQueuedAudioSize)(SDL_AudioDeviceID); void (*ClearQueuedAudio)(SDL_AudioDeviceID); + void (*GetVersion)(uint8_t v[3]); + const char *(*GetCurrentVideoDriver)(void); + const char *(*GetCurrentAudioDriver)(void); } sdl; /* 1: SDL is there and a display is (the game draws in a window); 0: headless */ @@ -105,7 +108,7 @@ static int sdl_on(void) "SDL_NumJoysticks", "SDL_IsGameController", "SDL_GameControllerOpen", "SDL_GameControllerClose", "SDL_GameControllerGetAttached", "SDL_GameControllerName", "SDL_GameControllerGetButton", "SDL_GameControllerGetAxis", "SDL_OpenAudioDevice", "SDL_CloseAudioDevice", "SDL_PauseAudioDevice", "SDL_QueueAudio", "SDL_GetQueuedAudioSize", - "SDL_ClearQueuedAudio", + "SDL_ClearQueuedAudio", "SDL_GetVersion", "SDL_GetCurrentVideoDriver", "SDL_GetCurrentAudioDriver", }; void **fp = (void **)&sdl; for (unsigned i = 0; i < sizeof names / sizeof names[0]; i++) @@ -116,6 +119,12 @@ static int sdl_on(void) uya_log("SDL2: cannot start (%s)", sdl.GetError()); return 0; } + { + uint8_t v[3] = {0, 0, 0}; + const char *vd = sdl.GetCurrentVideoDriver(), *ad = sdl.GetCurrentAudioDriver(); + sdl.GetVersion(v); + uya_log("SDL %d.%d.%d: video %s, audio %s", v[0], v[1], v[2], vd ? vd : "none", ad ? ad : "none"); + } sdl_state = 1; return 1; } diff --git a/games/rac1/host/src/platform_win32.c b/games/rac1/host/src/platform_win32.c index d064abc..312e4ce 100644 --- a/games/rac1/host/src/platform_win32.c +++ b/games/rac1/host/src/platform_win32.c @@ -180,11 +180,37 @@ static LONG CALLBACK on_exception(EXCEPTION_POINTERS *ep) return EXCEPTION_CONTINUE_SEARCH; } +#include +/* abort() (a library giving up) ends the process without an exception: raise one, so the + * crash handler reports it with the call chain */ +static void on_abort(int sig) +{ + (void)sig; + uya_log("crash: abort() was called"); + RaiseException(0xE0000ABE, EXCEPTION_NONCONTINUABLE, 0, 0); +} int plat_install_trap_handler(void) { + signal(SIGABRT, on_abort); return AddVectoredExceptionHandler(1, on_exception) ? 0 : -1; } +void plat_log_system(void) +{ + typedef struct { ULONG size, major, minor, build, platform; WCHAR sp[128]; } osv_t; + HMODULE nt = GetModuleHandleA("ntdll.dll"); + LONG (WINAPI *rtl_ver)(osv_t *) = nt ? (void *)GetProcAddress(nt, "RtlGetVersion") : 0; + const char *(*wine_ver)(void) = nt ? (void *)GetProcAddress(nt, "wine_get_version") : 0; + osv_t v; memset(&v, 0, sizeof v); v.size = sizeof v; + if (rtl_ver) rtl_ver(&v); + SYSTEM_INFO si; GetNativeSystemInfo(&si); + MEMORYSTATUSEX ms; memset(&ms, 0, sizeof ms); ms.dwLength = sizeof ms; GlobalMemoryStatusEx(&ms); + char cpu[128] = "?"; DWORD cl = sizeof cpu; + RegGetValueA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "ProcessorNameString", RRF_RT_REG_SZ, 0, cpu, &cl); + uya_log("system: Windows %lu.%lu build %lu%s%s, %s, %lu cores, %llu MB RAM", v.major, v.minor, v.build, + wine_ver ? ", Wine " : "", wine_ver ? wine_ver() : "", cpu, si.dwNumberOfProcessors, (unsigned long long)(ms.ullTotalPhys >> 20)); +} + uint64_t plat_time_us(void) { static LARGE_INTEGER f; diff --git a/games/rac1/host/src/runtime.c b/games/rac1/host/src/runtime.c index 6a00ca5..2be40b3 100644 --- a/games/rac1/host/src/runtime.c +++ b/games/rac1/host/src/runtime.c @@ -109,6 +109,11 @@ void uya_log_open(void) { char p[1024]; snprintf(p, sizeof p, "%s/rac1_log.txt", plat_exe_dir()); + { /* the previous run's log is kept as rac1_log.prev.txt (a retry after a crash keeps the crash) */ + char q[1100]; + snprintf(q, sizeof q, "%s/rac1_log.prev.txt", plat_exe_dir()); + remove(q); rename(p, q); + } logfile = fopen(p, "w"); if (logfile) fprintf(stderr, "[uya] log file: %s\n", p); } diff --git a/launcher/linux/project-solana b/launcher/linux/project-solana index daf609c..d3d535f 100755 --- a/launcher/linux/project-solana +++ b/launcher/linux/project-solana @@ -6,6 +6,7 @@ project-solana status what is installed, and whether the 32-bit libraries it needs are there project-solana desktop add Project Solana to your applications menu (and so to Steam) project-solana log print the last game log (rac1_log.txt) + project-solana report write one file with everything needed for a bug report Like the Windows launcher: nothing from the game is downloaded. The build files (the recompiler, the runtime, the decomp's source) come from the project's GitHub release, the @@ -25,12 +26,43 @@ OTHER = {"SCUS_971.99": "Ratchet & Clank (NTSC-U)", "SCPS_150.37": "Ratchet & Cl "SCUS_973.53": "Ratchet & Clank: Up Your Arsenal (NTSC-U)", "SCES_524.56": "Ratchet & Clank 3 (PAL)", "SCUS_974.65": "Ratchet: Deadlocked (NTSC-U)", "SCES_532.85": "Ratchet: Gladiator (PAL)"} +VERSION = "1.1" HOME = os.path.join(os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"), "project-solana") INI = os.path.join(HOME, "solana.ini") +LAUNCHER_LOG = os.path.join(HOME, "launcher_log.txt") -def say(*a): print(*a, flush=True) -def die(msg): sys.exit("project-solana: " + msg) +def log(text): + """everything the launcher says also goes to launcher_log.txt (started from Steam or a menu, + there is no terminal to read it in)""" + try: + os.makedirs(HOME, exist_ok=True) + if os.path.exists(LAUNCHER_LOG) and os.path.getsize(LAUNCHER_LOG) > (1 << 20): + os.replace(LAUNCHER_LOG, LAUNCHER_LOG + ".old") + with open(LAUNCHER_LOG, "a") as f: f.write(time.strftime("%Y-%m-%d %H:%M:%S ") + text + "\n") + except OSError: + pass + + +def notify(text): + """a desktop notification, when there is no terminal to print to""" + if sys.stdout.isatty(): return + for cmd in (["notify-send", "-a", "Project Solana", "Project Solana", text], ["zenity", "--error", "--title=Project Solana", "--text=" + text], + ["kdialog", "--title", "Project Solana", "--error", text]): + if shutil.which(cmd[0]): + try: subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL); return + except OSError: pass + + +def say(*a): + t = " ".join(str(x) for x in a) + print(t, flush=True); log(t) + + +def die(msg): + log("error: " + msg) + notify(msg + "\n\n(project-solana report writes a file to send with a bug report)") + sys.exit("project-solana: " + msg) def settings(): @@ -141,15 +173,20 @@ def have_lib(name): return False +def lib_paths(name): + return [os.path.join(d, name) for d in LIBDIRS + ["/lib", "/usr/lib"] if os.path.exists(os.path.join(d, name))] + + def check_libs(quiet=False): missing = [n for n in ("libSDL2-2.0.so.0", "libGL.so.1") if not have_lib(n)] + if not lib_paths("ld-linux.so.2"): missing.insert(0, "the 32-bit C library (ld-linux.so.2)") if missing and not quiet: say("\nThe game is a 32-bit program and needs these 32-bit libraries: %s" % ", ".join(missing)) say("Installing Steam brings them. Otherwise:") - say(" Debian, Ubuntu, Mint, Pop!_OS: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt install libsdl2-2.0-0:i386 libgl1:i386 libgl1-mesa-dri:i386") - say(" Fedora: sudo dnf install SDL2.i686 mesa-libGL.i686 mesa-dri-drivers.i686") - say(" Arch, Manjaro, EndeavourOS: enable [multilib], then sudo pacman -S lib32-sdl2 lib32-mesa") - say(" openSUSE: sudo zypper install libSDL2-2_0-0-32bit Mesa-libGL1-32bit") + say(" Debian, Ubuntu, Mint, Pop!_OS: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt install libc6:i386 libsdl2-2.0-0:i386 libgl1:i386 libgl1-mesa-dri:i386") + say(" Fedora: sudo dnf install glibc.i686 SDL2.i686 mesa-libGL.i686 mesa-dri-drivers.i686") + say(" Arch, Manjaro, EndeavourOS: enable [multilib], then sudo pacman -S lib32-glibc lib32-sdl2 lib32-mesa") + say(" openSUSE: sudo zypper install glibc-32bit libSDL2-2_0-0-32bit Mesa-libGL1-32bit") say(" (NVIDIA's own driver: also its 32-bit OpenGL library, e.g. lib32-nvidia-utils on Arch)") return not missing @@ -208,6 +245,7 @@ def build(args): if e != "build": rmtree(os.path.join(gdir, e)) rmtree(os.path.join(gdir, "build", "linux", "gen")); rmtree(os.path.join(gdir, "build", "linux", "obj")) unzip(zp, gdir, "build kit"); os.remove(zp) + open(kv, "w").write(man["version"]) # (unpacked: an interrupted build resumes with what it compiled) levels = c.get("build", "levels", fallback="") or man.get("levels") or ",".join(str(i) for i in range(19)) cpus = os.cpu_count() or 2 @@ -227,7 +265,6 @@ def build(args): p.wait(); log.close(); print() if p.returncode: die("the build stopped (see %s)" % os.path.join(gdir, "build_log.txt")) for j in ("build/linux/gen", "build/linux/gen.new", "build/linux/obj", "build/levels", "build/disc", "zig-cache"): rmtree(os.path.join(gdir, j)) - open(kv, "w").write(man["version"]) c.set("disc", kit, iso); c.set("build", "built_" + kit, man["version"]); save(c) say("Built. Ready to play: project-solana play") @@ -257,8 +294,21 @@ def play(args): if full: env["UYA_FULLSCREEN"] = "1" if pc.get("vsync", "0") in ("1", "True", "true"): env["UYA_VSYNC"] = "1" if not check_libs(quiet=True): check_libs(); die("install the 32-bit libraries above, then play again") - os.chdir(os.path.dirname(exe)) - os.execve(exe, [exe, "-", iso], env) + gdir = os.path.dirname(exe) + out = os.path.join(gdir, "game_output.txt") # what SDL, the graphics driver and the game print (kept: one run back) + if os.path.exists(out): os.replace(out, out + ".prev") + log("play: %s (UYA_SCALE=%s, fullscreen %s, vsync %s)" % (iso, env.get("UYA_SCALE", "1"), full, pc.get("vsync", "0"))) + with open(out, "w") as o: + try: + p = subprocess.Popen([exe, "-", iso], cwd=gdir, env=env, stdout=o, stderr=subprocess.STDOUT) + except OSError as e: + die("the game could not start (%s): run project-solana status" % e) + try: rc = p.wait() + except KeyboardInterrupt: p.terminate(); rc = p.wait() + import signal + if rc == 0: log("play: the game closed normally"); return + why = ("it crashed (%s)" % signal.Signals(-rc).name) if rc < 0 and -rc in [s.value for s in signal.Signals] else "it stopped with an error (code %d)" % rc + die("the game closed: %s. project-solana report writes a file to send with a bug report" % why) def status(args): @@ -291,8 +341,74 @@ def show_log(args): sys.stdout.write(open(p, errors="replace").read()) +def tail(path, n): + try: + with open(path, errors="replace") as f: lines = f.readlines() + except OSError: + return "(none)\n" + return ("(... %d earlier lines left out)\n" % (len(lines) - n) if len(lines) > n else "") + "".join(lines[-n:]) + + +def report(args): + """one text file with what a bug report needs; nothing from the game, and no file of the user's""" + c = settings() + gdir = os.path.join(HOME, "games", "rac1") + bdir = os.path.join(gdir, "build", "linux") + out = [] + w = out.append + w("Project Solana bug report (Linux launcher %s), %s\n" % (VERSION, time.strftime("%Y-%m-%d %H:%M:%S %z"))) + u = os.uname() + pretty = "?" + try: + for l in open("/etc/os-release"): + if l.startswith("PRETTY_NAME="): pretty = l.split("=", 1)[1].strip().strip('"') + except OSError: pass + w("\n== system\nLinux %s %s, %s\n" % (u.release, u.machine, pretty)) + try: + cpu = next((l.split(":", 1)[1].strip() for l in open("/proc/cpuinfo") if l.startswith("model name")), "?") + mem = next((l.split(":", 1)[1].strip() for l in open("/proc/meminfo") if l.startswith("MemTotal")), "?") + except OSError: cpu = mem = "?" + w("CPU %s, %d cores, RAM %s\n" % (cpu, os.cpu_count() or 0, mem)) + for k in ("XDG_SESSION_TYPE", "XDG_CURRENT_DESKTOP", "DISPLAY", "WAYLAND_DISPLAY", "SteamDeck", "SteamAppId"): + w("%s=%s\n" % (k, os.environ.get(k, ""))) + for cmd in (["lspci"], ["glxinfo", "-B"]): + if shutil.which(cmd[0]): + try: + r = subprocess.run(cmd, capture_output=True, text=True, timeout=10).stdout + if cmd[0] == "lspci": r = "".join(l + "\n" for l in r.splitlines() if "VGA" in l or "3D" in l or "Display" in l) + w("-- %s\n%s" % (" ".join(cmd), r)) + except Exception: pass + w("\n== 32-bit libraries\n") + for n in ("ld-linux.so.2", "libSDL2-2.0.so.0", "libGL.so.1"): + w("%s: %s\n" % (n, ", ".join(lib_paths(n)) or ("found by ldconfig" if have_lib(n) else "MISSING"))) + w("\n== Project Solana\ndata folder: %s\n" % HOME) + tv = os.path.join(HOME, "toolchain", "version.txt") + w("build tools: %s\n" % (open(tv).read().strip() if os.path.exists(tv) else "not downloaded")) + kv = os.path.join(gdir, "kit_version.txt") + w("build kit: %s\n" % (open(kv).read().strip() if os.path.exists(kv) else "none")) + w("game built: %s\n" % ("yes" if os.path.exists(os.path.join(bdir, "rac1_wrapper")) else "no")) + iso = c.get("disc", "rac1", fallback="") + w("disc image: %s\n" % ((("found, %.2f GB" % (os.path.getsize(iso) / 1e9)) if os.path.exists(iso) else "MISSING (moved?)") if iso else "none")) + w("settings:\n") + for s in c.sections(): + for k, v in c.items(s): + if s == "disc" or k == "release_url": continue + w(" [%s] %s = %s\n" % (s, k, v)) + for title, path, n in (("launcher log", LAUNCHER_LOG, 60), ("build log (end)", os.path.join(gdir, "build_log.txt"), 80), + ("game log", os.path.join(bdir, "rac1_log.txt"), 400), ("game log, the run before", os.path.join(bdir, "rac1_log.prev.txt"), 150), + ("game output (SDL, graphics driver)", os.path.join(bdir, "game_output.txt"), 120), + ("game output, the run before", os.path.join(bdir, "game_output.txt.prev"), 60)): + w("\n== %s\n%s" % (title, tail(path, n))) + text = "".join(out).replace(os.path.expanduser("~"), "~") # (no user name in the report) + dest_dir = next((d for d in (os.path.expanduser("~/Desktop"), os.path.expanduser("~")) if os.path.isdir(d)), HOME) + dest = os.path.join(dest_dir, "project-solana-report-%s.txt" % time.strftime("%Y%m%d-%H%M%S")) + open(dest, "w").write(text) + say("Wrote %s: attach it to your bug report (it has your system details and the logs, nothing else)." % dest) + + def main(): ap = argparse.ArgumentParser(prog="project-solana", description="Build Ratchet & Clank from your own disc, then play it.") + ap.add_argument("--version", action="version", version="project-solana %s (Linux launcher)" % VERSION) sp = ap.add_subparsers(dest="cmd") b = sp.add_parser("build", help="build the game from your disc image") b.add_argument("disc"); b.add_argument("-j", "--jobs", type=int, default=0) @@ -310,9 +426,20 @@ def main(): v.add_argument("--no-vsync", dest="vsync", action="store_const", const=0) sp.add_parser("status", help="what is installed"); sp.add_parser("desktop", help="add it to the applications menu") sp.add_parser("log", help="print the last game log") + sp.add_parser("report", help="write one file with everything a bug report needs") a = ap.parse_args() if sys.platform != "linux": die("this is the Linux launcher: on Windows, use Project Solana.exe") - {"build": build, "play": play, "status": status, "desktop": desktop, "log": show_log}.get(a.cmd, lambda _: ap.print_help())(a) + log("project-solana %s: %s" % (VERSION, " ".join(sys.argv[1:]))) + try: + {"build": build, "play": play, "status": status, "desktop": desktop, "log": show_log, "report": report}.get(a.cmd, lambda _: ap.print_help())(a) + except SystemExit: + raise + except KeyboardInterrupt: + sys.exit(130) + except Exception as e: # (an unexpected error: in the launcher log too, with where) + import traceback + log("unexpected error:\n" + traceback.format_exc()) + die("unexpected error: %s (details in %s)" % (e, LAUNCHER_LOG)) if __name__ == "__main__":