diff --git a/main.py b/main.py old mode 100644 new mode 100755 index 000a1d7..8f714a7 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ -#!/usr/bin/python +#!/usr/bin/python3 import os import errno -from re import T +import socket from shutil import rmtree import gi import urllib.request @@ -14,9 +14,15 @@ gi.require_version("Gtk", "3.0") gi.require_version("Handy", "1") -gi.require_version("AppIndicator3", "0.1") +try: + gi.require_version("AppIndicator3", "0.1") +except (ValueError, ImportError): + gi.require_version("AyatanaAppIndicator3", "0.1") gi.require_version("Notify", "0.7") -from gi.repository import Gtk, AppIndicator3 as appindicator +try: + from gi.repository import Gtk, AppIndicator3 as appindicator +except (ValueError, ImportError): + from gi.repository import Gtk, AyatanaAppIndicator3 as appindicator from gi.repository import GLib from gi.repository import GObject, Handy from gi.repository import GdkPixbuf @@ -75,6 +81,19 @@ def connectioncheck(): return False +def port_open(host, port, timeout=1.0): + """Return True if a TCP connection to host:port succeeds within `timeout`. + + Used to probe the local anisette-server without spawning curl/subshells. + No external dependencies, no shell, fast. + """ + try: + with socket.create_connection((host, port), timeout=timeout): + return True + except OSError: + return False + + # Check version with open(resource_path("resources/version"), "r", encoding="utf-8") as f: LocalVersion = f.readline().strip() @@ -297,72 +316,104 @@ def wait_for_t(self, t): def lol321actualfunction(self): global installedcheck - self.lbl1.set_text("Checking if anisette_server is already running...") - self.loadaltlinux.set_fraction(0.1) - command = 'curl 127.0.0.1:6969 | grep -q "{"' - CheckRun = subprocess.run(command, shell=True) - if not os.path.isfile(f"{(altlinuxpath)}/anisette_server"): - self.lbl1.set_text("Downloading anisette_server...") - r = requests.get( - "https://github.com/Dadoum/Provision/releases/download/1.1.0/anisette_server-x86_64", - allow_redirects=True, - ) - open(f"{(altlinuxpath)}/anisette_server", "wb").write(r.content) - subprocess.run(f"chmod +x {(altlinuxpath)}/anisette_server", shell=True) - subprocess.run(f"chmod 755 {(altlinuxpath)}/anisette_server", shell=True) - self.loadaltlinux.set_fraction(0.2) - self.lbl1.set_text("Downloading Apple Music APK...") - r = requests.get( - "https://apps.mzstatic.com/content/android-apple-music-apk/applemusic.apk", - allow_redirects=True, - ) - open(f"{(altlinuxpath)}/am.apk", "wb").write(r.content) - os.makedirs(f"{(altlinuxpath)}/lib/x86_64") - self.loadaltlinux.set_fraction(0.3) - self.lbl1.set_text("Extracting necessary libraries...") - CheckRunB = subprocess.run( - f'unzip -j "{(altlinuxpath)}/am.apk" "lib/x86_64/libstoreservicescore.so" -d "{(altlinuxpath)}/lib/x86_64"', - shell=True, - ) - CheckRunC = subprocess.run( - f'unzip -j "{(altlinuxpath)}/am.apk" "lib/x86_64/libCoreADI.so" -d "{(altlinuxpath)}/lib/x86_64"', - shell=True, - ) - silentremove(f"{(altlinuxpath)}/am.apk") - self.loadaltlinux.set_fraction(0.4) - self.lbl1.set_text("Starting anisette_server...") - subprocess.run(f"cd {(altlinuxpath)} && ./anisette_server &", shell=True) - self.loadaltlinux.set_fraction(0.5) - finished = False - while not finished: - CheckRun5 = subprocess.run(command, shell=True) - if CheckRun5.returncode == 0: - finished = True - else: - sleep(1) - if not os.path.isfile(f"{(altlinuxpath)}/AltServer"): - self.lbl1.set_text("Downloading AltServer...") - self.loadaltlinux.set_fraction(0.6) - r = requests.get( - "https://github.com/NyaMisty/AltServer-Linux/releases/download/v0.0.5/AltServer-x86_64", - allow_redirects=True, - ) - open(f"{(altlinuxpath)}/AltServer", "wb").write(r.content) - subprocess.run(f"chmod +x {(altlinuxpath)}/AltServer", shell=True) - subprocess.run(f"chmod 755 {(altlinuxpath)}/AltServer", shell=True) - if not os.path.isfile(f"{(altlinuxpath)}/AltStore.ipa"): - self.lbl1.set_text("Downloading AltStore...") - self.loadaltlinux.set_fraction(0.7) - r = requests.get( - "https://cdn.altstore.io/file/altstore/altstore-beta.ipa", - allow_redirects=True, - ) - open(f"{(altlinuxpath)}/AltStore.ipa", "wb").write(r.content) - subprocess.run(f"chmod 755 {(altlinuxpath)}/AltStore.ipa", shell=True) - self.lbl1.set_text("Starting AltServer...") - self.loadaltlinux.set_fraction(1.0) - subprocess.run(f"{(altlinuxpath)}/AltServer &", shell=True) - return 0 + try: + self.lbl1.set_text("Checking if anisette_server is already running...") + self.loadaltlinux.set_fraction(0.1) + anisette_already_up = port_open("127.0.0.1", 6969) + if not os.path.isfile(f"{(altlinuxpath)}/anisette_server"): + self.lbl1.set_text("Downloading anisette_server...") + r = requests.get( + "https://github.com/Dadoum/Provision/releases/download/2.0.0/anisette-server-x86_64", + allow_redirects=True, + ) + r.raise_for_status() + open(f"{(altlinuxpath)}/anisette_server", "wb").write(r.content) + subprocess.run(f"chmod +x {(altlinuxpath)}/anisette_server", shell=True) + subprocess.run(f"chmod 755 {(altlinuxpath)}/anisette_server", shell=True) + self.loadaltlinux.set_fraction(0.2) + self.lbl1.set_text("Downloading Apple Music APK...") + r = requests.get( + "https://apps.mzstatic.com/content/android-apple-music-apk/applemusic.apk", + allow_redirects=True, + ) + r.raise_for_status() + open(f"{(altlinuxpath)}/am.apk", "wb").write(r.content) + os.makedirs(f"{(altlinuxpath)}/lib/x86_64") + self.loadaltlinux.set_fraction(0.3) + self.lbl1.set_text("Extracting necessary libraries...") + CheckRunB = subprocess.run( + f'unzip -j "{(altlinuxpath)}/am.apk" "lib/x86_64/libstoreservicescore.so" -d "{(altlinuxpath)}/lib/x86_64"', + shell=True, + ) + CheckRunC = subprocess.run( + f'unzip -j "{(altlinuxpath)}/am.apk" "lib/x86_64/libCoreADI.so" -d "{(altlinuxpath)}/lib/x86_64"', + shell=True, + ) + silentremove(f"{(altlinuxpath)}/am.apk") + self.loadaltlinux.set_fraction(0.4) + # If anisette wasn't already serving on :6969, start it ourselves. + if not anisette_already_up: + self.lbl1.set_text("Starting anisette_server...") + # Start the server detached so the worker thread can return + # even if the server takes a moment to bind. + with open(f"{(altlinuxpath)}/anisette.log", "ab") as anisette_log: + subprocess.Popen( + f"cd {(altlinuxpath)} && exec ./anisette_server", + shell=True, + stdout=anisette_log, + stderr=anisette_log, + start_new_session=True, + ) + self.loadaltlinux.set_fraction(0.5) + # Bounded wait: probe port 6969 every second for up to 60s. + # Previously this used `curl 127.0.0.1:6969 | grep -q "{"`, + # which spins forever if curl isn't on PATH or if the server + # never binds (e.g. Apple's CDN blocks the library download). + wait_ok = False + for _ in range(60): + if port_open("127.0.0.1", 6969): + wait_ok = True + break + sleep(1) + if not wait_ok: + raise RuntimeError( + "anisette_server did not start listening on 127.0.0.1:6969 " + "within 60s. See ~/.local/share/altlinux/anisette.log for details." + ) + if not os.path.isfile(f"{(altlinuxpath)}/AltServer"): + self.lbl1.set_text("Downloading AltServer...") + self.loadaltlinux.set_fraction(0.6) + r = requests.get( + "https://github.com/NyaMisty/AltServer-Linux/releases/download/v0.0.5/AltServer-x86_64", + allow_redirects=True, + ) + r.raise_for_status() + open(f"{(altlinuxpath)}/AltServer", "wb").write(r.content) + subprocess.run(f"chmod +x {(altlinuxpath)}/AltServer", shell=True) + subprocess.run(f"chmod 755 {(altlinuxpath)}/AltServer", shell=True) + if not os.path.isfile(f"{(altlinuxpath)}/AltStore.ipa"): + self.lbl1.set_text("Downloading AltStore...") + self.loadaltlinux.set_fraction(0.7) + r = requests.get( + "https://cdn.altstore.io/file/altstore/altstore-beta.ipa", + allow_redirects=True, + ) + r.raise_for_status() + open(f"{(altlinuxpath)}/AltStore.ipa", "wb").write(r.content) + subprocess.run(f"chmod 755 {(altlinuxpath)}/AltStore.ipa", shell=True) + self.lbl1.set_text("Starting AltServer...") + self.loadaltlinux.set_fraction(1.0) + subprocess.run(f"{(altlinuxpath)}/AltServer &", shell=True) + return 0 + except Exception as exc: # noqa: BLE001 - we want to show the user anything + # Without this guard, an exception on the worker thread is + # silently swallowed by Python and the splash window freezes + # forever. Surface the real cause instead. + GLib.idle_add(self._show_bootstrap_error, repr(exc)) + return 1 + + def _show_bootstrap_error(self, message): + openwindow(OopsAnisette, message=message) class login(Gtk.Window): @@ -927,6 +978,91 @@ def on_info_clicked2(self, widget): quitit() +class OopsAnisette(Handy.Window): + """Error window shown when bootstrap (download + anisette start) fails. + + Unlike Oops/OopsInternet this carries the real exception text so the + user (or a support thread) can see *why* it failed instead of staring + at a frozen splash screen forever. + """ + + def __init__(self, message=""): + super().__init__(title="Error") + self.present() + self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) + self.set_resizable(False) + self.set_size_request(520, 200) + self.set_border_width(10) + + # WindowHandle + handle = Handy.WindowHandle() + self.add(handle) + box = Gtk.VBox() + vb = Gtk.VBox(spacing=0, orientation=Gtk.Orientation.VERTICAL) + + # Headerbar + self.hb = Handy.HeaderBar() + self.hb.set_show_close_button(True) + self.hb.props.title = "Error" + vb.pack_start(self.hb, False, True, 0) + + pixbuf = Gtk.IconTheme.get_default().load_icon( + "dialog-error-symbolic", 48, 0 + ) + image = Gtk.Image.new_from_pixbuf(pixbuf) + image.show() + image.set_margin_top(10) + vb.pack_start(image, True, True, 0) + + lbl1 = Gtk.Label( + label=( + "AltLinux could not finish setting itself up.\n" + "anisette-server did not start, or a required download failed." + ) + ) + lbl1.set_property("margin_left", 15) + lbl1.set_property("margin_right", 15) + lbl1.set_justify(Gtk.Justification.CENTER) + lbl1.set_margin_top(10) + + # Render the real exception text in a scrollable, monospaced view + # so long tracebacks stay readable. Escape any markup so a hostile + # error string can't break the label. + scroller = Gtk.ScrolledWindow() + scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scroller.set_property("margin_left", 15) + scroller.set_property("margin_right", 15) + scroller.set_margin_top(8) + scroller.set_size_request(480, 140) + + detail = Gtk.TextView() + detail.set_editable(False) + detail.set_cursor_visible(False) + detail.set_monospace(True) + detail.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + detail.get_buffer().set_text( + (message or "Unknown error.").replace("<", "<") + ) + scroller.add(detail) + + button = Gtk.Button(label="Quit") + button.set_property("margin_left", 125) + button.set_property("margin_right", 125) + button.set_margin_top(10) + button.connect("clicked", self.on_info_clicked2) + + handle.add(vb) + vb.pack_start(lbl1, expand=False, fill=True, padding=0) + vb.pack_start(scroller, True, True, 0) + vb.pack_start(button, False, False, 10) + box.add(vb) + self.add(box) + self.show_all() + + def on_info_clicked2(self, widget): + quitit() + + def notify(): if (connectioncheck()) == True: LatestVersion = ( @@ -972,8 +1108,8 @@ def showurl(_): quitit() -def openwindow(window): - w = window() +def openwindow(window, **kwargs): + w = window(**kwargs) w.show_all()