Allow users to export PowerShell scripts

This commit is contained in:
Glavo 2021-10-15 23:04:15 +08:00 committed by Yuhui Huang
parent 12368375cf
commit 67feca075b
9 changed files with 215 additions and 51 deletions

View File

@ -29,10 +29,7 @@ import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
import org.jackhuang.hmcl.launch.PermissionException;
import org.jackhuang.hmcl.launch.ProcessCreationException;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
@ -49,6 +46,7 @@ import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
@ -284,6 +282,23 @@ public final class LauncherHelper {
message = i18n("download.code.404", url);
else
message = i18n("download.failed", url, responseCode);
} else if (ex instanceof CommandTooLongException) {
message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) {
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
(result, resolve, reject) -> {
if (CommandBuilder.setExecutionPolicy()) {
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
resolve.run();
} else {
LOG.warning("Failed to set ExecutionPolicy");
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
}
})
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
);
return;
} else {
message = StringUtils.getStackTrace(ex);
}

View File

@ -191,6 +191,7 @@ public final class Versions {
chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
File file = chooser.showSaveDialog(Controllers.getStage());
if (file != null)
new LauncherHelper(profile, account, id).makeLaunchScript(file);

View File

@ -292,7 +292,8 @@ download.javafx.prepare=Ready to download
extension.bat=Windows Bat file
extension.mod=Mod file
extension.png=Image file
extension.sh=Bash shell
extension.ps1=PowerShell Script
extension.sh=Bash Script
fatal.javafx.incompatible=Application cannot patch JavaFX on current Java environment below 11.\nPlease run this app using JDK 11 or higher or a JDK with JavaFX bundled.
fatal.javafx.missing=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Oracle JRE 8, or install BellSoft Liberica Full JRE.\nIf you are using other OpenJDK builds, please ensure OpenJFX is included.
@ -454,9 +455,13 @@ launch.advice.vanilla_x86.translation=Minecraft currently does not provide offic
launch.failed=Unable to launch
launch.failed.cannot_create_jvm=Java virtual machine could not be created. Java arguments may cause issues. Please restart without JVM arguments.
launch.failed.creating_process=Failed to create process. Check your Java path.
launch.failed.command_too_long=The command length exceeds the limit so cannot create bat script. Please export it as PowerShell script.
launch.failed.decompressing_natives=Unable to decompress native libraries.
launch.failed.download_library=Unable to download library %s.
launch.failed.executable_permission=Unable to add permission to the launch script
launch.failed.executable_permission=Unable to add permission to the launch script.
launch.failed.execution_policy=Set execution policy
launch.failed.execution_policy.failed_to_set=Failed to set execution policy
launch.failed.execution_policy.hint=The current execution policy prevents running of PowerShell scripts.\nClick 'Ok' to allow the current user to execute PowerShell scripts,or click 'Cancel' to keep the status quo.
launch.failed.exited_abnormally=Game exited abnormally, please check the log, or ask someone for help.
launch.failed.no_accepted_java=Cannot find the Java installation suitable for current game. If you think you have installed a suitable Java VM, you can manually select it in game settings.
launch.state.dependencies=Dependencies

View File

@ -292,6 +292,7 @@ download.javafx.prepare=準備開始下載
extension.bat=Windows 指令碼
extension.mod=模組檔案
extension.png=圖片檔案
extension.ps1=PowerShell 指令碼
extension.sh=Bash 指令碼
fatal.javafx.incompatible=缺少 JavaFX 運行環境。\nHMCL 無法在低於 Java 11 且缺少 JavaFX 的 Java 環境上自行補全 JavaFX 運行環境。\n請更換如 Oracle Java 8 等包含 JavaFX 的 Java 運行環境,或者更新到 Java 11 或更高版本。
@ -454,9 +455,13 @@ launch.advice.vanilla_x86.translation=Minecraft 官方尚未提供對非 x86-64
launch.failed=啟動失敗
launch.failed.cannot_create_jvm=偵測到無法建立 Java 虛擬機,可能是 Java 參數有問題。可以在設定中開啟無參數模式啟動。
launch.failed.creating_process=啟動失敗,在建立新處理程式時發生錯誤。可能是 Java 路徑錯誤。
launch.failed.command_too_long=命令長度超過限制,無法創建 bat 腳本,請匯出為 PowerShell 腳本。
launch.failed.decompressing_natives=無法解壓縮遊戲資源庫。
launch.failed.download_library=無法下載遊戲相依元件 %s。
launch.failed.executable_permission=無法為啟動檔案新增執行權限。
launch.failed.execution_policy=設定執行策略
launch.failed.execution_policy.failed_to_set=設定執行策略失敗
launch.failed.execution_policy.hint=當前執行策略封锁您執行 PowerShell 腳本。\n點擊“確定”允許當前用戶執行本地 PowerShell 腳本,或點擊“取消”保持現狀。
launch.failed.exited_abnormally=遊戲非正常退出,請查看記錄檔案,或聯絡他人尋求幫助。
launch.failed.no_accepted_java=找不到適合當前遊戲使用的 Java。如果您認為實際存在合適的 Java您可以在遊戲設置中手動設置 Java。
launch.state.dependencies=處理遊戲相依元件

View File

@ -292,6 +292,7 @@ download.javafx.prepare=准备开始下载
extension.bat=Windows 脚本
extension.mod=模组文件
extension.png=图片文件
extension.ps1=PowerShell 脚本
extension.sh=Bash 脚本
fatal.javafx.incompatible=缺少 JavaFX 运行环境。\nHMCL 无法在低于 Java 11 且缺少 JavaFX 的 Java 环境上自行补全 JavaFX 运行环境。\n请更换如 Oracle Java 8 等包含 JavaFX 的 Java 运行环境,或者更新到 Java 11 或更高版本。
@ -454,9 +455,13 @@ launch.advice.vanilla_x86.translation=Minecraft 官方尚未提供对非 x86-64
launch.failed=启动失败
launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是 Java 路径错误。
launch.failed.command_too_long=命令长度超过限制,无法创建 bat 脚本,请导出为 PowerShell 脚本。
launch.failed.decompressing_natives=未能解压游戏本地库。
launch.failed.download_library=未能下载游戏依赖 %s.
launch.failed.executable_permission=未能为启动文件添加执行权限。
launch.failed.execution_policy=设置执行策略
launch.failed.execution_policy.failed_to_set=设置执行策略失败
launch.failed.execution_policy.hint=当前执行策略阻止您执行 PowerShell 脚本。\n点击“确定”允许当前用户执行本地 PowerShell 脚本,或点击“取消”保持现状。
launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。
launch.failed.no_accepted_java=找不到适合当前游戏使用的 Java。如果您认为实际存在合适的 Java您可以在游戏设置中手动设置 Java。
launch.state.dependencies=处理游戏依赖

View File

@ -0,0 +1,18 @@
package org.jackhuang.hmcl.launch;
public class CommandTooLongException extends RuntimeException {
public CommandTooLongException() {
}
public CommandTooLongException(String message) {
super(message);
}
public CommandTooLongException(String message, Throwable cause) {
super(message, cause);
}
public CommandTooLongException(Throwable cause) {
super(cause);
}
}

View File

@ -38,13 +38,11 @@ import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Bits;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.function.Supplier;
@ -438,7 +436,7 @@ public class DefaultLauncher extends Launcher {
public void makeLaunchScript(File scriptFile) throws IOException {
boolean isWindows = OperatingSystem.WINDOWS == OperatingSystem.CURRENT_OS;
File nativeFolder = null;
File nativeFolder;
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
nativeFolder = repository.getNativeDirectory(version.getId(), options.getJava().getPlatform());
} else {
@ -449,54 +447,112 @@ public class DefaultLauncher extends Launcher {
decompressNatives(nativeFolder);
}
if (isWindows && !FileUtils.getExtension(scriptFile).equals("bat"))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'bat' in Windows");
else if (!isWindows && !FileUtils.getExtension(scriptFile).equals("sh"))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'sh' in macOS/Linux");
String scriptExtension = FileUtils.getExtension(scriptFile);
boolean usePowerShell = "ps1".equals(scriptExtension);
if (!usePowerShell) {
if (isWindows && !scriptExtension.equals("bat"))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'bat' or 'ps1' in Windows");
else if (!isWindows && !scriptExtension.equals("sh"))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'sh' or 'ps1' in macOS/Linux");
}
if (!FileUtils.makeFile(scriptFile))
throw new IOException("Script file: " + scriptFile + " cannot be created.");
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(scriptFile), OperatingSystem.NATIVE_CHARSET))) {
if (isWindows) {
writer.write("@echo off");
writer.newLine();
writer.write("set APPDATA=" + options.getGameDir().getAbsoluteFile().getParent());
writer.newLine();
for (Map.Entry<String, String> entry : getEnvVars().entrySet()) {
writer.write("set " + entry.getKey() + "=" + entry.getValue());
writer.newLine();
}
writer.newLine();
writer.write(new CommandBuilder().add("cd", "/D", repository.getRunDirectory(version.getId()).getAbsolutePath()).toString());
writer.newLine();
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX || OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
writer.write("#!/usr/bin/env bash");
writer.newLine();
for (Map.Entry<String, String> entry : getEnvVars().entrySet()) {
writer.write("export " + entry.getKey() + "=" + entry.getValue());
writer.newLine();
}
writer.write(new CommandBuilder().add("cd", repository.getRunDirectory(version.getId()).getAbsolutePath()).toString());
writer.newLine();
}
if (StringUtils.isNotBlank(options.getPreLaunchCommand())) {
writer.write(options.getPreLaunchCommand());
writer.newLine();
}
writer.write(generateCommandLine(nativeFolder).toString());
writer.newLine();
if (StringUtils.isNotBlank(options.getPostExitCommand())) {
writer.write(options.getPostExitCommand());
writer.newLine();
}
if (isWindows) {
writer.write("pause");
final CommandBuilder commandLine = generateCommandLine(nativeFolder);
final String command = usePowerShell ? null : commandLine.toString();
if (!usePowerShell && isWindows) {
if (command.length() > 8192) { // maximum length of the command in cmd
throw new CommandTooLongException();
}
}
OutputStream outputStream = new FileOutputStream(scriptFile);
Charset charset = StandardCharsets.UTF_8;
if (isWindows) {
if (usePowerShell) {
// Write UTF-8 BOM
try {
outputStream.write(0xEF);
outputStream.write(0xBB);
outputStream.write(0xBF);
} catch (IOException e) {
outputStream.close();
throw e;
}
} else {
charset = OperatingSystem.NATIVE_CHARSET;
}
}
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, charset))) {
if (usePowerShell) {
if (isWindows) {
writer.write("$Env:APPDATA = ");
writer.write(CommandBuilder.pwshString(options.getGameDir().getAbsoluteFile().getParent()));
writer.newLine();
}
for (Map.Entry<String, String> entry : getEnvVars().entrySet()) {
writer.write("$Env:" + entry.getKey() + " = ");
writer.write(CommandBuilder.pwshString(entry.getValue()));
writer.newLine();
}
writer.write("Set-Location -Path ");
writer.write(CommandBuilder.pwshString(repository.getRunDirectory(version.getId()).getAbsolutePath()));
writer.newLine();
writer.write('&');
for (String rawCommand : commandLine.asList()) {
writer.write(' ');
writer.write(CommandBuilder.pwshString(rawCommand));
}
} else {
if (isWindows) {
writer.write("@echo off");
writer.newLine();
writer.write("set APPDATA=" + options.getGameDir().getAbsoluteFile().getParent());
writer.newLine();
for (Map.Entry<String, String> entry : getEnvVars().entrySet()) {
writer.write("set " + entry.getKey() + "=" + entry.getValue());
writer.newLine();
}
writer.newLine();
writer.write(new CommandBuilder().add("cd", "/D", repository.getRunDirectory(version.getId()).getAbsolutePath()).toString());
} else {
writer.write("#!/usr/bin/env bash");
writer.newLine();
for (Map.Entry<String, String> entry : getEnvVars().entrySet()) {
writer.write("export " + entry.getKey() + "=" + entry.getValue());
writer.newLine();
}
writer.write(new CommandBuilder().add("cd", repository.getRunDirectory(version.getId()).getAbsolutePath()).toString());
}
writer.newLine();
if (StringUtils.isNotBlank(options.getPreLaunchCommand())) {
writer.write(options.getPreLaunchCommand());
writer.newLine();
}
writer.write(generateCommandLine(nativeFolder).toString());
writer.newLine();
if (StringUtils.isNotBlank(options.getPostExitCommand())) {
writer.write(options.getPostExitCommand());
writer.newLine();
}
if (isWindows) {
writer.write("pause");
writer.newLine();
}
}
}
if (!scriptFile.setExecutable(true))
throw new PermissionException();
if (usePowerShell && !CommandBuilder.hasExecutionPolicy()) {
throw new ExecutionPolicyLimitException();
}
}
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {

View File

@ -0,0 +1,18 @@
package org.jackhuang.hmcl.launch;
public final class ExecutionPolicyLimitException extends RuntimeException {
public ExecutionPolicyLimitException() {
}
public ExecutionPolicyLimitException(String message) {
super(message);
}
public ExecutionPolicyLimitException(String message, Throwable cause) {
super(message, cause);
}
public ExecutionPolicyLimitException(Throwable cause) {
super(cause);
}
}

View File

@ -19,7 +19,10 @@ package org.jackhuang.hmcl.util.platform;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -165,6 +168,44 @@ public final class CommandBuilder {
}
}
public static String pwshString(String str) {
return "'" + str.replace("'", "''") + "'";
}
public static boolean hasExecutionPolicy() {
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS) {
return true;
}
try {
final Process process = Runtime.getRuntime().exec("powershell -Command Get-ExecutionPolicy");
if (!process.waitFor(5, TimeUnit.SECONDS)) {
process.destroy();
return false;
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
String policy = reader.readLine();
return "Unrestricted".equalsIgnoreCase(policy) || "RemoteSigned".equalsIgnoreCase(policy);
}
} catch (Throwable ignored) {
return false;
}
}
public static boolean setExecutionPolicy() {
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS) {
return true;
}
try {
final Process process = Runtime.getRuntime().exec(new String[]{"powershell", "-Command", "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser"});
if (!process.waitFor(5, TimeUnit.SECONDS)) {
process.destroy();
return false;
}
} catch (Throwable ignored) {
}
return true;
}
private static String parseBatch(String s) {
String escape = " \t\"^&<>|";
if (StringUtils.containsOne(s, escape.toCharArray()))