Linux diagnostics updates
This commit is contained in:
+137
-10
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user