diff --git a/DMMGamePlayerFastLauncher.py b/DMMGamePlayerFastLauncher.py index e2c193b..ec285c8 100644 --- a/DMMGamePlayerFastLauncher.py +++ b/DMMGamePlayerFastLauncher.py @@ -11,6 +11,8 @@ import os import time from urllib.parse import urlparse +import win32security +import sys class DgpSession: @@ -211,6 +213,12 @@ class ErrorManager: url="https://github.com/fa0311/DMMGamePlayerFastLauncher/issues", ) + elevate_admin_error: ErrorManagerType = ErrorManagerType( + message="Failed to elevate to administrator privileges.", + solution="Report an Issues.", + url="https://github.com/fa0311/DMMGamePlayerFastLauncher/issues", + ) + def error(self, error: ErrorManagerType, log: str | None = None): output = filter( lambda x: x != None, @@ -225,7 +233,86 @@ def info(self, text: str): print(text) +class ProcessManager: + non_request_admin: bool = False + non_bypass_uac: bool = False + error_manager: ErrorManager + + def __init__(self, error_manager: ErrorManager) -> None: + self.error_manager = error_manager + + def run( + self, args: dict[str], admin: bool = False, force: bool = False + ) -> subprocess.Popen[bytes] | None: + print(" ".join(args)) + if admin: + if not self.non_bypass_uac and not force: + run_bypass_uac() + elif self.non_request_admin: + self.error_manager.error(error=ErrorManager.permission_error) + else: + args = [f'"{arg}"' for arg in args] + ctypes.windll.shell32.ShellExecuteW( + None, "runas", args[0], " ".join(args[1:]), None, 1 + ) + else: + return subprocess.Popen( + args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + +def get_sid() -> str: + desc = win32security.GetFileSecurity(".", win32security.OWNER_SECURITY_INFORMATION) + sid = desc.GetSecurityDescriptorOwner() + sidstr = win32security.ConvertSidToStringSid(sid) + return sidstr + + +def run_bypass_uac(): + schtasks_file = "schtasks_v1_" + os.getlogin() + schtasks_name = f"\Microsoft\Windows\DMMGamePlayerFastLauncher\{schtasks_file}" + + run_args = [arg.schtasks_path, "/run", "/tn", schtasks_name] + + if process_manager.run(run_args).wait() == 1: + schtasks_xml_path = ( + r"{appdata}\DMMGamePlayerFastLauncher\assets\{name}.xml".format( + appdata=os.environ["APPDATA"], name=schtasks_file + ) + ) + schtasks_task_path = ( + r"{appdata}\DMMGamePlayerFastLauncher\Tools\Task.exe".format( + appdata=os.environ["APPDATA"] + ) + ) + with open("assets/template.xml", "r") as f: + template = f.read() + + with open(f"assets/{schtasks_file}.xml", "w") as f: + f.write( + template.replace(r"{{UID}}", schtasks_file) + .replace(r"{{SID}}", get_sid()) + .replace(r"{{COMMAND}}", schtasks_task_path) + .replace(r"{{WORKING_DIRECTORY}}", os.getcwd()) + ) + + create_args = [ + arg.schtasks_path, + "/create", + "/xml", + schtasks_xml_path, + "/tn", + schtasks_name, + ] + process_manager.run(create_args, admin=True, force=True) + time.sleep(3) + process_manager.run(run_args).wait() + time.sleep(5) + sys.exit() + + error_manager = ErrorManager() +process_manager = ProcessManager(error_manager) argpar = argparse.ArgumentParser( prog="DMMGamePlayerFastLauncher", @@ -239,9 +326,14 @@ def info(self, text: str): argpar.add_argument("--skip-exception", action="store_true") argpar.add_argument("--https-proxy-uri", default=None) argpar.add_argument("--non-request-admin", action="store_true") +argpar.add_argument("--non-bypass-uac", action="store_true") +argpar.add_argument("--schtasks-path", default="schtasks.exe") + try: arg = argpar.parse_args() error_manager.skip = arg.skip_exception + process_manager.non_request_admin = arg.non_request_admin + process_manager.non_bypass_uac = arg.non_bypass_uac except: error_manager.error(error=ErrorManager.argument_error) @@ -324,20 +416,17 @@ def info(self, text: str): dmm_args = dmm_args + arg.game_args.split(" ") print(game_path) start_time = time.time() - process = subprocess.Popen( - [game_path] + dmm_args, shell=True, stdout=subprocess.PIPE - ) + process = process_manager.run([game_path] + dmm_args) for line in process.stdout: text = line.decode("utf-8").strip() print(text) - if time.time() - start_time < 2 and not arg.skip_exception: + if time.time() - start_time < 2: if response["data"]["is_administrator"]: - if not ctypes.windll.shell32.IsUserAnAdmin() and not arg.non_request_admin: - ctypes.windll.shell32.ShellExecuteW( - None, "runas", game_path, response["data"]["execute_args"], None, 1 - ) - else: - error_manager.error(error=ErrorManager.permission_error) + process = process_manager.run([game_path] + dmm_args, admin=True) + else: + error_manager.error( + error=ErrorManager.startup_error, log=json.dumps(response) + ) elif response["result_code"] == 307: error_manager.error(error=ErrorManager.auth_device_error) diff --git a/DMMGamePlayerProductIdChecker.py b/DMMGamePlayerProductIdChecker.py index 4737260..f05bb15 100644 --- a/DMMGamePlayerProductIdChecker.py +++ b/DMMGamePlayerProductIdChecker.py @@ -17,5 +17,5 @@ pd.set_option("display.unicode.east_asian_width", True) print(df) -print("終了するには何かキーを押してください") +print("Please press any key to exit") input() diff --git a/README.md b/README.md index e995d2f..3507a4c 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,12 @@ DMM Game Player のゲームを高速かつセキュアに起動できるラン ## 特徴 - **ワンクリックでゲームを起動** +- **高速** - **最低限の権限で起動** +- **UAC の自動昇格** - **DMM にハードウェア情報を送信しない** - **隠されているコマンドライン引数を使用可能** +- **面倒な設定の必要無し** ## インストール diff --git a/Task.py b/Task.py new file mode 100644 index 0000000..3a63afc --- /dev/null +++ b/Task.py @@ -0,0 +1,22 @@ +import psutil +import os +import signal +import ctypes + + +for p in psutil.process_iter(attrs=("name", "pid", "cmdline")): + if p.info["name"] != "DMMGamePlayerFastLauncher.exe": + continue + + cmdline = p.info["cmdline"] + if "--non-bypass-uac" not in cmdline: + cmdline.append("--non-bypass-uac") + cmdline = [f'"{cmd}"' for cmd in cmdline] + os.kill(p.info["pid"], signal.SIGTERM) + print("killed " + " ".join(cmdline)) + ctypes.windll.shell32.ShellExecuteW( + None, "runas", cmdline[0], " ".join(cmdline[1:]), None, 1 + ) + break +else: + print("Error") diff --git a/build.ps1 b/build.ps1 index 4377b6c..150ee42 100644 --- a/build.ps1 +++ b/build.ps1 @@ -2,6 +2,8 @@ black *.py pip freeze > requirements.txt pyinstaller DMMGamePlayerFastLauncher.py --onefile --noconsole pyinstaller DMMGamePlayerProductIdChecker.py --onefile +pyinstaller Task.py --onefile --noconsole New-Item "Z:\Project\Python\hack\DMMGamePlayerFastLauncher\windows\tools" -ItemType Directory -Force Copy-Item -Path "Z:\Project\Python\hack\DMMGamePlayerFastLauncher\dist\DMMGamePlayerProductIdChecker.exe" -Destination "Z:\Project\Python\hack\DMMGamePlayerFastLauncher\windows\tools" -Force +Copy-Item -Path "Z:\Project\Python\hack\DMMGamePlayerFastLauncher\dist\Task.exe" -Destination "Z:\Project\Python\hack\DMMGamePlayerFastLauncher\windows\tools" -Force Start-Process "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "Z:\Project\Python\hack\DMMGamePlayerFastLauncher\setup.iss" \ No newline at end of file diff --git a/docs/README-advance.md b/docs/README-advance.md index 4084323..d4d8fc1 100644 --- a/docs/README-advance.md +++ b/docs/README-advance.md @@ -19,26 +19,51 @@ `DMMGamePlayerFastLauncher.exe ` -| オプション | エイリアス | デフォルト | タイプ | -| ------------------- | ---------- | ---------- | ------------------ | -| --help | -h | False | Bool | -| --game-path | | None | String | None | -| --game-args | | None | String | None | -| --login-force | | Flase | Bool | -| --skip-exception | | False | Bool | -| --https-proxy-uri | | None | String | None | -| --non-request-admin | | False | Bool | +| オプション | エイリアス | デフォルト | タイプ | note | +| ------------------- | ---------- | ------------ | ------------------ | ---------- | +| --help | -h | False | Bool | | +| --game-path | | None | String | None | | +| --game-args | | None | String | None | | +| --login-force | | Flase | Bool | deprecated | +| --skip-exception | | False | Bool | | +| --https-proxy-uri | | None | String | None | | +| --non-request-admin | | False | Bool | deprecated | +| --non-bypass-uac | | False | Bool | | +| --schtasks-path | | schtasks.exe | String | | + +```mermaid + graph TD; + ゲームのプロセスを開始 --権限不備で起動しない--> non-bypass-uac; + ゲームのプロセスを開始 --Start/Error--> 続行; + non-bypass-uac --false--> 最初のリクエスト; + 最初のリクエスト --no-->ゲームに権限与える + 最初のリクエスト --yes--> non-request-admin + non-bypass-uac --true--> non-request-admin; + non-request-admin --true--> skip-exception; + non-request-admin --false--> UAC; + UAC --allow--> ゲームに権限与える + UAC --disabled--> 続行 + skip-exception --true--> 続行 + skip-exception --false--> エラー +``` + +**最初のリクエスト** - 最初のリクエストは、管理者権限を要求します +これは、タスクスケジューラーに自動的に権限を昇格させるプログラムを登録するために必要です + +特にこだわりが無ければ product_id 以外の引数は不要です +ゲームや環境によっては`game-path` `https-proxy-uri` `schtasks-path` などが必要です ### game-path -何も指定していない場合は自動で検出しますがゲームによってはうまくいかない場合があります +ゲームのパスを指定します +何も指定していない場合は自動で検出しますがうまくいかない場合は指定してください 例: `%AppData%\DMMGamePlayerFastLauncher\DMMGamePlayerFastLauncher.exe umamusume --game-path %UserProfile%/umamusume/umamusume.exe` ### game-args -ゲームに引数を追加したい場合はこれを指定します +ゲームに引数を追加したい場合はこの引数を使用します 通常の DMM を介した起動方法で使用できない隠された引数を使用することができます `"` で囲む必要があることに注意してください @@ -55,7 +80,7 @@ Unity 製ゲームの引数はここに詳しく載ってます ### skip-exception -エラーを出力しません +この引数を使用するとエラーを出力しなくなります これはあくまで応急処置で基本的には使わないで下さい 原因不明なエラーが発生した場合は [issues](https://github.com/fa0311/DMMGamePlayerFastLauncher/issues) に報告して下さい @@ -79,12 +104,46 @@ Socks5 ### non-request-admin -このツールは管理者権限を必要なときのみ要求することがありますがそれを要求しなくなります +この引数を使用すると管理者権限を要求しません ほとんどの場合、この引数は不要です 例: `%AppData%\DMMGamePlayerFastLauncher\DMMGamePlayerFastLauncher.exe umamusume --non-request-admin` +### non-bypass-uac + +この引数を使用すると UAC の自動昇格を行わなくなります + +指定していない場合はタスクスケジューラを使った権限の自動昇格を行います +タスクの詳細はこのコマンドで確認できます +複雑な処理を行うため数秒程度起動速度が遅くなる場合があります +`schtasks.exe /query /tn \Microsoft\Windows\DMMGamePlayerFastLauncher\` + +また、このコマンドで削除できます +`Get-ScheduledTask | where TaskPath -eq "\Microsoft\Windows\DMMGamePlayerFastLauncher" | Unregister-ScheduledTask -Confirm:$false` +または `.\tools\refresh.ps1` + +### schtasks-path + +`schtasks.exe`のパスを指定します +ほとんどの場合、この引数は不要です + +## ファイル階層 + +| ファイル名 | 削除 | 詳細 | +| --------------------------------------- | ---- | ------------------------------------------------------------------------------ | +| DMMGamePlayerFastLauncher.exe | x | 本体 | +| unins000.dat | o | アンインストールする際に必要 | +| unins000.exe | x | アンインストールする際に必要 | +| cookie.bytes | o | セッションのキャッシュファイル, DMM のセッションが取得できなかった際に使用する | +| tools/DMMGamePlayerProductIdChecker.exe | o | プロダクト ID のチェッカー,左ダブルクリックで実行 | +| tools/Task.exe | x | タスクスケジューラから呼び出される | +| tools/refresh.ps1 | o | タスクを削除する,右クリックメニューから PowerShell で実行 | +| sample/ウマ娘.lnk | o | ウマ娘の起動ショートカット,左ダブルクリックで実行 | +| sample/プリコネ R.lnk | o | プリコネ R の起動ショートカット,左ダブルクリックで実行 | +| assets/template.xml | x | タスクスケジューラのテンプレートファイル | +| assets/schtasks_v1\_{username}.xml | o | タスクスケジューラの残骸ファイル | + ## ヘルプ ### セットアップする際、「Windows によって PC が保護されました」と表示される diff --git a/requirements.txt b/requirements.txt index 9d6d749..10b94f1 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/setup.iss b/setup.iss index b8a1a1d..349907d 100644 --- a/setup.iss +++ b/setup.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "DMMGamePlayerFastLauncher" -#define MyAppVersion "4.5.2" +#define MyAppVersion "4.6.0" #define MyAppPublisher "yuki" #define MyAppURL "https://github.com/fa0311/DMMGamePlayerFastLauncher" #define MyAppExeName "DMMGamePlayerFastLauncher.exe" @@ -21,7 +21,7 @@ AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} -DefaultDirName={autopf}\{#MyAppName} +DefaultDirName={userappdata}\{#MyAppName} DisableDirPage=yes ChangesAssociations=yes DefaultGroupName={#MyAppName} @@ -34,6 +34,7 @@ OutputBaseFilename=DMMGamePlayerFastLauncher-Setup Compression=lzma SolidCompression=yes WizardStyle=modern +UninstallFilesDir={userappdata}\{#MyAppName} [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" @@ -41,16 +42,16 @@ Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl" Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" [Files] -Source: "Z:\Project\Python\hack\DMMGamePlayerFastLauncher\dist\{#MyAppExeName}"; DestDir: "{userappdata}\{#MyAppName}"; Flags: ignoreversion -Source: "Z:\Project\Python\hack\DMMGamePlayerFastLauncher\windows\*"; DestDir: "{userappdata}\{#MyAppName}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "Z:\Project\Python\hack\DMMGamePlayerFastLauncher\dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "Z:\Project\Python\hack\DMMGamePlayerFastLauncher\windows\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Registry] Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppAssocName}"; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{userappdata}\{#MyAppName}\{#MyAppExeName},0" -Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{userappdata}\{#MyAppName}\{#MyAppExeName}"" ""%1""" +Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0" +Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1""" Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".myp"; ValueData: "" [Icons] -Name: "{group}\{#MyAppName}"; Filename: "{userappdata}\{#MyAppName}\{#MyAppExeName}" +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" \ No newline at end of file diff --git a/windows/assets/template.xml b/windows/assets/template.xml new file mode 100644 index 0000000..c6d3236 --- /dev/null +++ b/windows/assets/template.xml @@ -0,0 +1,44 @@ + + + + 2023-01-01T00:00:00 + fa0311 + uac_bypass_v1 + \Microsoft\Windows\DMMGamePlayerFastLauncher\{{UID}} + + + + + {{SID}} + InteractiveToken + HighestAvailable + + + + Parallel + false + false + false + false + false + + true + false + + true + true + false + false + false + true + false + PT0S + 7 + + + + {{COMMAND}} + {{WORKING_DIRECTORY}} + + + \ No newline at end of file diff --git a/windows/tools/refresh.ps1 b/windows/tools/refresh.ps1 new file mode 100644 index 0000000..32d74dc --- /dev/null +++ b/windows/tools/refresh.ps1 @@ -0,0 +1,2 @@ +if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Administrators")) { Start-Process powershell.exe "-File `"$PSCommandPath`" -NoNewWindow -Wait" -Verb RunAs; exit } +Get-ScheduledTask | where TaskPath -eq "\Microsoft\Windows\DMMGamePlayerFastLauncher\" | Unregister-ScheduledTask -Confirm:$false \ No newline at end of file