diff --git a/WSA_OneClickRun.zip b/WSA_OneClickRun.zip new file mode 100644 index 0000000..221954f Binary files /dev/null and b/WSA_OneClickRun.zip differ diff --git a/functions.py b/functions.py new file mode 100644 index 0000000..1b09f0f --- /dev/null +++ b/functions.py @@ -0,0 +1,193 @@ +import os +import time +import shutil +import tkinter +from tkinter.font import Font +import ctypes +import sys +from sys import exit +from urllib import request, error +from speed_downloader import speed_download + + +def download_url(url, root=".", filename=None): + """Download a file from a url and place it in root. + Args: + url (str): URL to download file from + root (str): Directory to place downloaded file in + filename (str, optional): Name to save the file under. If None, use the basename of the URL + """ + + root = os.path.expanduser(root) + if not filename: + filename = os.path.basename(url) + fpath = os.path.join(root, filename) + + os.makedirs(root, exist_ok=True) + try: + print('Downloading ' + url + ' to ' + fpath) + request.urlretrieve(url, fpath) + except (error.URLError, IOError): + if url[:5] == 'https': + url = url.replace('https:', 'http:') + print('Failed download. Trying https -> http instead.' + ' Downloading ' + url + ' to ' + fpath) + request.urlretrieve(url, fpath) + + +def is_admin(): + """ + Checks if the user has admin permissions + :return: admin true/false + """ + try: + return ctypes.windll.shell32.IsUserAnAdmin() + except Exception as e: + print(e) + return False + + +def get_admin_permission(): + """checks if the program has admin permissions, otherwise it requests admin permission once, then exits""" + if is_admin(): + pass + else: + ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1) + exit() + + +def ps_check_feature(feature): + """Checks whether a Windows optional feature exists. + Returns True if the component is installed. Otherwise returns False. + Requires admin permissions.""" + _ = os.popen("powershell \"Get-WindowsOptionalFeature -FeatureName {} -Online\"".format(feature)).read().replace( + " ", "") + if "State:Disabled" in _: + return False + elif "State:Enabled" in _: + return True + else: + return False + + +def is_wsl_framework_installed(): + """Checks if the WSL framework (excluding distros) is installed.""" + return ps_check_feature("Microsoft-Windows-Subsystem-Linux") and ps_check_feature( + "VirtualMachinePlatform") and shutil.which("bash") and shutil.which("wsl") + + +def wsl_get_distro(): + """ + Returns the output of "wsl.exe --list" + :return: + """ + __ = os.popen("wsl --list").readlines() + result = (_.replace("\x00", "") for _ in __) + result = "".join(_ for _ in result if _ != "\n" and _ != "") + return result.casefold() + + +class WindowError(tkinter.Tk): + """ + A window displaying errors. May not be polished yet. Especially the quit() and destroy() not working properly. + """ + + def __init__(self, *messages, color="red", tx_color="white", yes_command="", yes_text="OK", no_text="Exit"): + """ + Creates the windows. Mainloop needed after initializing + :param messages: All the messages. Separate the strings to make a line break + :param color: Background color + :param tx_color: Text color + :param yes_command: Executes this command in a string. + :param yes_text: The label of the button. + """ + super().__init__() + self.title("WSAGAScript OneClickRun") + self.geometry("640x240") + self.minsize(height=240, width=640) + self.maxsize(height=240, width=640) + self.config(padx=8, pady=8, background=color) + row_weight = 10000, 1 + for _, __ in enumerate(row_weight): + self.rowconfigure(_, weight=__) + column_weight = 100, 1 + for _, __ in enumerate(column_weight): + self.columnconfigure(_, weight=__) + error_frame = tkinter.Frame(self, background=color) + error_frame.grid(row=0, column=0, sticky="new", columnspan=2) + for message in messages: + tkinter.Label(error_frame, text=message, font=Font(family="Arial", size=12), background=color, + fg=tx_color, wraplength=600, justify="left").pack(side="top", anchor="nw") + if yes_command: + button_accept = tkinter.Button(self, relief="raised", text=yes_text, command=self.accept_button_function) + self.yes_command = yes_command + button_accept.grid(row=1, column=0, sticky="e") + button_exit = tkinter.Button(self, relief="raised", text=no_text, command=self.destroy) + button_exit.grid(row=1, column=1, sticky="e") + + def accept_button_function(self): + # self.quit() + self.destroy() + exec(f'{self.yes_command}') + + +def install_wsl(): + """Installs WSL and its dependencies, then reboots. Requires admin permission.""" + if is_admin(): + print("Installing Windows Subsystem for Linux. The system will restart in about a minute.") + os.popen( + "dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart").read() + print("Installing Virtual Machine Platform. The system will restart in about 30 seconds.") + os.popen("dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart").read() + os.popen("shutdown.exe /r /t 0") + + +def is_linux_enabled(distro): + """ + Checks if WSL distro is installed. If not, tries to install distro. Requires admin + If WSL isn't present, prompts to install WSL and its prerequisites. + :param distro: + :return: TRUE if the specified WSL distro has already been installed. + LINUX_INSTALLED if WSL is present but the distro is not. + WSL_NOT_INSTALLED if WSL is not present. + """ + if not is_wsl_framework_installed(): + _ = WindowError("WSL framework is not installed.", + "Please save your work before clicking install. The machine will reboot.", + yes_text="Install WSL", + yes_command="install_wsl()", + no_text="Exit") + _.wait_window() + exit() + wsl_list_header = "windows subsystem for linux distributions" + wsl_no_distributions = "no installed distributions" + result = wsl_get_distro() + if distro in result and wsl_list_header in result: # checks if distro has been installed. Done + os.popen("wsl -s {}".format(distro)) + return "TRUE" + elif wsl_no_distributions in result or distro not in result and wsl_list_header in result: + # checks if there is no distribution, or the distribution is not present + print("An instance of {} is being installed on your device. Please wait.".format(distro)) + print("Downloading kernel.") + speed_download("https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi", "./TEMP") + os.popen("msiexec /i \"TEMP\\wsl_update_x64.msi\" /quiet").read() + print(f"Downloading {distro} system image.") + os.popen("wsl --install -d {}".format(distro)).read() + # execution of the last line stops, but installation hasn't been completed. therefore must check again. + while True: # check again for the presence of the OS before exiting + time.sleep(2) + result = wsl_get_distro() + # print(result) + if distro in result and wsl_list_header in result: + os.popen("wsl -s {}".format(distro)) + break + return "LINUX_INSTALLED" + + +def remove(path): + """ param could either be relative or absolute. """ + if os.path.exists(path): + if os.path.isfile(path) or os.path.islink(path): + os.remove(path) # remove the file + elif os.path.isdir(path): + shutil.rmtree(path) # remove dir and all contains diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..be276e3 --- /dev/null +++ b/setup.py @@ -0,0 +1,204 @@ +import platform +from functions import * +import os +import shutil +import traceback +from sys import exit +from wsa_online_link_generator import * +import fnmatch +from speed_downloader import speed_download +from xml.dom import minidom +from packaging import version +import subprocess + +# URL to download WSA Script from GitHub +wsagascript_url = "https://github.com/ADeltaX/WSAGAScript/archive/refs/heads/main.zip" + +# URLs to download GApps from SourceForge. Hardcoded :( +gapps_url_x64 = "https://nchc.dl.sourceforge.net/project/opengapps/x86_64/20211021/open_gapps-x86_64-11.0-pico-20211021.zip" +gapps_url_arm64 = "https://nchc.dl.sourceforge.net/project/opengapps/arm64/20211030/open_gapps-arm64-11.0-pico-20211030.zip" + +# directories for system images +gapps_dir = "./TEMP/WSAGAScript-main/#GAPPS" +images_dir = "./TEMP/WSAGAScript-main/#IMAGES" + +# instead of installing in a temporary folder, move everything to install_loc before installing +install_loc = "C:/Program Files/WSA_Advanced/" + +# preinstalled version initialization +existing_install_version = None + +supported_architecture = ["arm64", "amd64"] + + +def cleanup(): + cur_dir = os.path.dirname(__file__) + os.chdir(cur_dir) + remove("./TEMP") + + +if __name__ == "__main__": + try: + # gets admin and modifies registry for developer mode, tries to enable WSL, + # and switches to the executable directory (just don't want to be execute in the wrong one, + # which may delete important stuff + get_admin_permission() + executable_dir = os.path.dirname(__file__) + # if the script executes in the shell with the location C:\, + # it will affect the folder C:\TEMP (which is NOT good) + print(f"EXECUTABLE DIRECTORY: {executable_dir}") + os.chdir(executable_dir) + + cpu_arch = platform.machine() + if cpu_arch.casefold() not in supported_architecture: + WindowError("Your CPU does not support Windows Subsystem for Android.").wait_window() + exit() + + print(f'WSL install status: {is_linux_enabled("debian")}') + + os.popen('reg add "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock"' + ' /t REG_DWORD' + ' /f /v "AllowDevelopmentWithoutDevLicense" /d "1"').read() + cleanup() + + # creates WSA directories + # Checks for updates and downloads the newest version of WSA. + os.makedirs("./TEMP/wsa", exist_ok=True) + os.makedirs("./TEMP/wsa_main", exist_ok=True) + os.makedirs(install_loc, exist_ok=True) + + # obtains WSA package link and version + wsa_entry_result = get_wsa_entry() + if not wsa_entry_result: + wsa_archive_version = None + WindowError("No matching Windows Subsystem for Android package was found. Press ENTER to exit.") + exit() + else: + wsa_archive_url, wsa_archive_name = wsa_entry_result + wsa_archive_version = version.parse(wsa_archive_name.split("_")[1]) + if not wsa_archive_version: + raise Exception("Sanity check failed. WSA archive version not found.") + try: + existing_install_version = max(map(version.parse, os.listdir(install_loc))) + # exception will be raised so that the new installation mode is used if there is nothing + if not existing_install_version: + # makes sure the version is not empty. Empty directories may cause unintentional deletions. + raise Exception("Sanity check failed. WSA existing version not found.") + print(f"Existing installation version: {existing_install_version}") + print(f'Latest version: {wsa_archive_version}') + if wsa_archive_version <= existing_install_version: + input("Windows Subsystem for Android is up-to-date. Press ENTER to exit.") + exit() + else: + print("Updating WSA...") + except ValueError: + print("New installation detected.") + speed_download(wsa_archive_url, "./TEMP", "wsa.zip") + shutil.unpack_archive("./TEMP/wsa.zip", "./TEMP/wsa", "zip") + + # unpacks the correct 64-bit archive + for _ in os.listdir("./TEMP/wsa"): + if cpu_arch.casefold() == "amd64" and fnmatch.fnmatch(_, "*x64_Release*.msix"): + shutil.unpack_archive(f"./TEMP/wsa/{_}", "./TEMP/wsa_main", "zip") + break + elif cpu_arch.casefold() == "arm64" and fnmatch.fnmatch(_, "*ARM64_Release*.msix"): + shutil.unpack_archive(f"./TEMP/wsa/{_}", "./TEMP/wsa_main", "zip") + break + else: + cleanup() + WindowError("Your selected archive does not have the 64-bit MSIX bundle.").wait_window() + exit() + + # removes signature from package + remove("./TEMP/wsa_main/AppxMetadata") + remove("./TEMP/wsa_main/[Content_Types].xml") + remove("./TEMP/wsa_main/AppxBlockMap.xml") + remove("./TEMP/wsa_main/AppxSignature.p7x") + + # downloads WSAGAScript and extract, creates WSAGAScript-main + os.makedirs("./TEMP/WSAGAScript-main", exist_ok=True) + speed_download(wsagascript_url, "./TEMP", "WSAGAScript.zip") + shutil.unpack_archive("./TEMP/WSAGAScript.zip", "./TEMP", "zip") + + # creates GAPPS and IMAGES directory in case it's missing + os.makedirs(gapps_dir, exist_ok=True) + os.makedirs(images_dir, exist_ok=True) + + # downloads GApps + if cpu_arch.casefold() == "amd64": + speed_download(gapps_url_x64, gapps_dir) + else: + speed_download(gapps_url_arm64, gapps_dir) + + # moves files to working directory. + for _ in os.listdir("./TEMP/wsa_main"): + if fnmatch.fnmatch(_, "*.img"): + shutil.move(f'./TEMP/wsa_main/{_}', "./TEMP/WSAGAScript-main/#IMAGES") + + # executes main script + install_script = """#!/bin/bash + sudo apt update + sudo apt install unzip lzip + sudo bash ./extract_gapps_pico.sh + sudo bash ./extend_and_mount_images.sh + sudo bash ./apply.sh + sudo bash ./unmount_images.sh""" + with open("./TEMP/WSAGAScript-main/autorun.sh", "w", newline="\n") as file: + file.write(install_script) + print("ALTERING SYSTEM IMAGE. DO NOT EXIT!") + print(os.popen("bash -c 'cd ./TEMP/WSAGAScript-main; sudo bash ./autorun.sh'").read()) + + # copies back + for _ in os.listdir("./TEMP/WSAGAScript-main/#IMAGES"): + if fnmatch.fnmatch(_, "*.img"): + shutil.move(f'./TEMP/WSAGAScript-main/#IMAGES/{_}', "./TEMP/wsa_main") + + # rooted kernel + if cpu_arch.casefold() == "amd64": + os.rename('./TEMP/WSAGAScript-main/misc/kernel-x86_64', "./TEMP/kernel") + else: + os.rename('./TEMP/WSAGAScript-main/misc/kernel-arm64', "./TEMP/kernel") + shutil.copy("./TEMP/kernel", "./TEMP/wsa_main/Tools") + + # bypasses Windows 11 requirement + manifest_data = minidom.parse("./TEMP/wsa_main/AppxManifest.xml") + selected_element = manifest_data.getElementsByTagName("TargetDeviceFamily")[0] + selected_element.attributes["MinVersion"].value = "10.0.19043.1237" + + with open("./TEMP/wsa_main/AppxManifest.xml", "w", encoding="utf-8") as file: + file.write(manifest_data.toxml()) + + # installs + new_install_location = os.path.realpath(os.path.join(install_loc, str(wsa_archive_version))) + print(f'Installing to {new_install_location}') + os.makedirs(new_install_location, exist_ok=True) + + for file in os.listdir("./TEMP/wsa_main"): + shutil.move(os.path.join("./TEMP/wsa_main", file), new_install_location) + install_process = subprocess.run(f"powershell.exe Add-AppxPackage " + f"-Register '{new_install_location}\\AppXManifest.xml' " + f"-ForceTargetApplicationShutdown") + + # cleans up temporary folder + print("Cleaning up temporary files.") + cleanup() + + # deletes either the old or new version depending on return code + if not install_process.returncode: + if existing_install_version: + print(f"Deleting version {existing_install_version}.") + remove(os.path.join(install_loc, str(existing_install_version))) + WindowError("WSA with GApps and root access installed. Press ENTER to exit.", + color="green", tx_color="white").wait_window() + else: + remove(new_install_location) + WindowError("Package installation failed. Installation has been rolled back.").wait_window() + except Exception as e: + cleanup() + print(traceback.format_exc()) + WindowError("Install failure. An exception occured. Installation has been rolled back.", + tx_color="white").wait_window() + with open("error.log", "w") as file: + print(e, file=file) + print(traceback.format_exc(), file=file) + exit() diff --git a/speed_downloader.py b/speed_downloader.py new file mode 100644 index 0000000..f9021d6 --- /dev/null +++ b/speed_downloader.py @@ -0,0 +1,74 @@ +import asyncio +import concurrent.futures +import traceback + +import requests +import os + + +async def get_size(url): + response = requests.head(url) + size = int(response.headers['Content-Length']) + return size + + +def download_range(url, start, end, output): + headers = {'Range': f'bytes={start}-{end}'} + response = requests.get(url, headers=headers) + + with open(output, 'wb') as f: + for part in response.iter_content(1024): + f.write(part) + + +async def download(executor, url, output, chunk_size=1024768): + loop = asyncio.get_event_loop() + file_size = await get_size(url) + chunks = range(0, file_size, chunk_size) + + tasks = [ + loop.run_in_executor( + executor, + download_range, + url, + start, + start + chunk_size - 1, + f'{output}.part{i}', + ) + for i, start in enumerate(chunks) + ] + + await asyncio.wait(tasks) + + with open(output, 'wb') as o: + for i in range(len(chunks)): + chunk_path = f'{output}.part{i}' + + with open(chunk_path, 'rb') as s: + o.write(s.read()) + + os.remove(chunk_path) + + +def speed_download(url, root, filename=None): + executor = concurrent.futures.ThreadPoolExecutor(max_workers=8) + loop = asyncio.get_event_loop() + url = url + root = os.path.expanduser(root) + + if not filename: + filename = os.path.basename(url) + fpath = os.path.join(root, filename) + + os.makedirs(root, exist_ok=True) + print(f"Downloading {url} to {fpath}") + while True: + try: + loop.run_until_complete(download(executor, url, fpath)) + except Exception as e: + print(e) + print(traceback.format_exc()) + print("Trying download again.") + continue + else: + break \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..41cd603 --- /dev/null +++ b/test.py @@ -0,0 +1,12 @@ +# TESTING commands. No effect in app execution +import shutil +from functions import * +import platform + +cpu_arch = platform.machine() + +if cpu_arch.casefold() == "amd64": + os.rename('./TEMP/WSAGAScript-main/misc/kernel-x86_64', "./TEMP/kernel") +else: + os.rename('./TEMP/WSAGAScript-main/misc/kernel-arm64', "./TEMP/kernel") +shutil.copy("./TEMP/kernel", "./TEMP/wsa_main/Tools") \ No newline at end of file diff --git a/uninstall.py b/uninstall.py new file mode 100644 index 0000000..93bac73 --- /dev/null +++ b/uninstall.py @@ -0,0 +1,28 @@ +from functions import * +import subprocess +import os +import traceback +from sys import exit + +if __name__ == '__main__': + try: + get_admin_permission() + if not os.popen("powershell.exe Get-AppXPackage MicrosoftCorporationII.WindowsSubsystemForAndroid").read().strip(): + input("Windows Subsystem for Android is not installed. Press ENTER to exit.") + exit() + os.chdir(os.path.dirname(__file__)) + choice = input("Uninstall Windows Subsystem for Android? [Y]es [N]o (default: no) > ") + if choice.casefold() in ["y", "yes"]: + a = subprocess.run("powershell.exe Get-AppXPackage MicrosoftCorporationII.WindowsSubsystemForAndroid |" + " Remove-AppXPackage -AllUsers") + if not a.returncode: + for _ in os.listdir("C:/Program Files/WSA_Advanced"): + remove(os.path.join("C:/Program Files/WSA_Advanced", _)) + input("Windows Subsystem for Android uninstalled. Press ENTER to exit.") + else: + print("Windows Subsystem for Android failed to uninstall," + " or has already been uninstalled, or uninstallation canceled.") + input("Press ENTER to exit.") + except Exception as e: + print(traceback.format_exc()) + input("Press ENTER to exit.") \ No newline at end of file diff --git a/wsa_online_link_generator.py b/wsa_online_link_generator.py new file mode 100644 index 0000000..537e395 --- /dev/null +++ b/wsa_online_link_generator.py @@ -0,0 +1,30 @@ +import requests +from bs4 import BeautifulSoup +import fnmatch + + +def get_wsa_entry(): + """Returns a tuple containing the Windows Subsystem for Android link and its file name (used to check for + updates) """ + store_id = "9p3395vx91nr" + api_url = "https://store.rg-adguard.net/api/GetFiles" + data = { + "type": "ProductId", + "url": store_id, + "ring": "WIS", + "lang": "en-US", + } + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B179 Safari/7534.48.3", + } + r = requests.post(url=api_url, data=data, headers=headers) + print(r.text) + soup = BeautifulSoup(r.text, "html.parser") + for link in soup.select("tr a"): + if fnmatch.fnmatch(link.string.casefold(), "*windowssubsystemforandroid*.msixbundle"): + return link['href'], link.string + + +if __name__ == '__main__': + print(get_wsa_entry())