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

@@ -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()))