This commit is contained in:
crschnick 2025-04-07 11:14:22 +00:00
parent 04d48f2f78
commit 85617ee01c
15 changed files with 238 additions and 98 deletions

View file

@ -12,8 +12,6 @@ public class TerminalPrepareExchangeImpl extends TerminalPrepareExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
TerminalView.get().open(msg.getRequest(), msg.getPid());
TerminalLauncherManager.registerPid(msg.getRequest(), msg.getPid());
var term = AppPrefs.get().terminalType().getValue();
var unicode = term.supportsUnicode();
var escapes = term.supportsEscapes();

View file

@ -0,0 +1,25 @@
package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.TerminalPrepareExchange;
import io.xpipe.beacon.api.TerminalRegisterExchange;
public class TerminalRegisterExchangeImpl extends TerminalRegisterExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
TerminalView.get().open(msg.getRequest(), msg.getPid());
TerminalLauncherManager.registerPid(msg.getRequest(), msg.getPid());
return Response.builder()
.build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -147,7 +147,7 @@ public class OhMyPoshTerminalPrompt extends ConfigFileTerminalPrompt {
return true;
}
var extension = OsType.getLocal() == OsType.WINDOWS ? ".exe" : "";
var extension = sc.getOsType() == OsType.WINDOWS ? ".exe" : "";
return sc.view().fileExists(getBinaryDirectory(sc).join("oh-my-posh" + extension));
}

View file

@ -80,7 +80,7 @@ disabled = true
return true;
}
var extension = OsType.getLocal() == OsType.WINDOWS ? ".exe" : "";
var extension = sc.getOsType() == OsType.WINDOWS ? ".exe" : "";
return sc.view().fileExists(getBinaryDirectory(sc).join("starship" + extension));
}

View file

@ -17,8 +17,8 @@ import io.xpipe.core.process.ShellDialect;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.FilePath;
import lombok.Value;
import lombok.With;
import lombok.*;
import lombok.experimental.NonFinal;
import java.nio.file.Files;
import java.time.Instant;
@ -27,17 +27,19 @@ import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Value
@With
@RequiredArgsConstructor
@AllArgsConstructor
public class TerminalLaunchConfiguration {
DataStoreColor color;
String coloredTitle;
String cleanTitle;
boolean preferTabs;
FilePath scriptFile;
String scriptContent;
ShellDialect scriptDialect;
@NonFinal
FilePath scriptFile = null;
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").withZone(ZoneId.systemDefault());
@ -52,11 +54,10 @@ public class TerminalLaunchConfiguration {
var color = entry != null ? DataStorage.get().getEffectiveColor(entry) : null;
var d = ProcessControlProvider.get().getEffectiveLocalDialect();
var launcherScript = d.terminalLauncherScript(request, adjustedTitle, promptRestart);
var preparationScript = ScriptHelper.createLocalExecScript(launcherScript);
if (!AppPrefs.get().enableTerminalLogging().get()) {
var config = new TerminalLaunchConfiguration(
entry != null ? color : null, adjustedTitle, cleanTitle, preferTabs, preparationScript, d);
entry != null ? color : null, adjustedTitle, cleanTitle, preferTabs, launcherScript, d);
return config;
}
@ -86,15 +87,14 @@ public class TerminalLaunchConfiguration {
.formatted(
logFile.getFileName().toString(),
logFile,
preparationScript,
launcherScript,
logFile.getFileName().toString());
var ps = ScriptHelper.createExecScript(ShellDialects.POWERSHELL, sc, content);
var config = new TerminalLaunchConfiguration(
entry != null ? color : null,
adjustedTitle,
cleanTitle,
preferTabs,
ps,
content,
ShellDialects.POWERSHELL);
return config;
} else {
@ -114,23 +114,34 @@ public class TerminalLaunchConfiguration {
script -e -q "%s" "%s"
echo "Transcript stopped, output file is sessions/%s"
"""
.formatted(logFile.getFileName(), logFile, preparationScript, logFile.getFileName())
.formatted(logFile.getFileName(), logFile, launcherScript, logFile.getFileName())
: """
echo "Transcript started, output file is sessions/%s"
script --quiet --command "%s" "%s"
echo "Transcript stopped, output file is sessions/%s"
"""
.formatted(logFile.getFileName(), preparationScript, logFile, logFile.getFileName());
var ps = ScriptHelper.createExecScript(sc.getShellDialect(), sc, content);
.formatted(logFile.getFileName(), launcherScript, logFile, logFile.getFileName());
var config = new TerminalLaunchConfiguration(
entry != null ? color : null, adjustedTitle, cleanTitle, preferTabs, ps, sc.getShellDialect());
entry != null ? color : null, adjustedTitle, cleanTitle, preferTabs, content, sc.getShellDialect());
return config;
}
}
}
public CommandBuilder getDialectLaunchCommand() {
var open = scriptDialect.getOpenScriptCommand(scriptFile.toString());
public TerminalLaunchConfiguration withScript(ShellDialect d, String content) {
return new TerminalLaunchConfiguration(color, coloredTitle, cleanTitle, preferTabs, content, d);
}
@SneakyThrows
public synchronized FilePath getScriptFile() {
if (scriptFile == null) {
scriptFile = ScriptHelper.createExecScript(scriptDialect, LocalShell.getShell(), scriptContent);
}
return scriptFile;
}
public synchronized CommandBuilder getDialectLaunchCommand() {
var open = scriptDialect.getOpenScriptCommand(getScriptFile().toString());
return open;
}
}

View file

@ -24,7 +24,7 @@ public class TerminalLaunchRequest {
@Setter
@NonFinal
long pid;
long shellPid;
@Setter
@NonFinal
@ -94,8 +94,8 @@ public class TerminalLaunchRequest {
};
try {
var command = TerminalLauncher.createLaunchCommand(processControl, config, wd);
var file = ScriptHelper.createLocalExecScript(command);
var openCommand = processControl.prepareTerminalOpen(config, wd);
var file = ScriptHelper.createLocalExecScript(openCommand);
setResult(new TerminalLaunchResult.ResultSuccess(file.asLocalPath()));
} catch (Exception e) {
setResult(new TerminalLaunchResult.ResultFailure(e));

View file

@ -1,6 +1,7 @@
package io.xpipe.app.terminal;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
@ -10,10 +11,13 @@ import io.xpipe.app.util.ScriptHelper;
import io.xpipe.core.process.*;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.FailableFunction;
import io.xpipe.core.util.XPipeInstallation;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
public class TerminalLauncher {
@ -27,6 +31,21 @@ public class TerminalLauncher {
TerminalInitScriptConfig config,
boolean exit)
throws Exception {
var content = constructTerminalInitScript(t, processControl, workingDirectory, preInit, postInit, config, exit);
var hash = ScriptHelper.getScriptHash(content);
var file = t.getInitFileName(processControl, hash);
return ScriptHelper.createExecScriptRaw(processControl, file, content);
}
public static String constructTerminalInitScript(
ShellDialect t,
ShellControl processControl,
WorkingDirectoryFunction workingDirectory,
List<String> preInit,
List<String> postInit,
TerminalInitScriptConfig config,
boolean exit)
throws Exception {
String nl = t.getNewLine().getNewLineString();
var content = "";
@ -64,10 +83,7 @@ public class TerminalLauncher {
}
content = t.prepareScriptContent(content);
var hash = ScriptHelper.getScriptHash(content);
var file = t.getInitFileName(processControl, hash);
return ScriptHelper.createExecScriptRaw(processControl, file, content);
return content;
}
public static void openDirect(
@ -94,7 +110,7 @@ public class TerminalLauncher {
String title, FailableFunction<ShellControl, String, Exception> command, ExternalTerminalType type)
throws Exception {
try (var sc = LocalShell.getShell().start()) {
var script = constructTerminalInitFile(
var script = constructTerminalInitScript(
sc.getShellDialect(),
sc,
WorkingDirectoryFunction.none(),
@ -145,20 +161,42 @@ public class TerminalLauncher {
&& AppPrefs.get().clearTerminalOnInit().get()
&& !AppPrefs.get().developerPrintInitFiles().get(),
cc instanceof ShellControl ? type.additionalInitCommands() : TerminalInitFunction.none());
var promptRestart = AppPrefs.get().terminalPromptForRestart().getValue()
&& AppPrefs.get().terminalMultiplexer().getValue() == null;
var promptRestart = AppPrefs.get().terminalPromptForRestart().getValue();
var latch = TerminalLauncherManager.submitAsync(request, cc, terminalConfig, directory);
var config = TerminalLaunchConfiguration.create(
request, entry, cleanTitle, adjustedTitle, preferTabs, promptRestart);
var latch = TerminalLauncherManager.submitAsync(request, cc, terminalConfig, directory);
try {
if (!checkMultiplexerLaunch(cc, request, config)) {
if (preferTabs
&& TerminalMultiplexerManager.getEffectiveMultiplexer().isPresent()) {
config =
config.withPreferTabs(false).withCleanTitle("XPipe").withColoredTitle("XPipe");
}
type.launch(config);
if (preferTabs && launchMultiplexerTabInExistingTerminal(request, terminalConfig, config)) {
latch.await();
return;
}
if (preferTabs) {
var multiplexerConfig = launchMultiplexerTabInNewTerminal(request, terminalConfig, config);
if (multiplexerConfig.isPresent()) {
launch(type, multiplexerConfig.get(), latch);
return;
}
}
var proxyConfig = launchProxy(request, terminalConfig, config);
if (proxyConfig.isPresent()) {
launch(type, proxyConfig.get(), latch);
return;
}
var initScript = AppPrefs.get().terminalInitScript().getValue();
var customInit = initScript != null ? initScript + "\n" : "";
config = config.withScript(
ProcessControlProvider.get().getEffectiveLocalDialect(),
getTerminalRegisterCommand(request) + "\n" + customInit + "\n" + config.getScriptContent());
launch(type, config, latch);
}
private static void launch(ExternalTerminalType type, TerminalLaunchConfiguration config, CountDownLatch latch) throws Exception {
try {
type.launch(config);
latch.await();
} catch (Exception ex) {
var modMsg = ex.getMessage() != null && ex.getMessage().contains("Unable to find application named")
@ -169,12 +207,12 @@ public class TerminalLauncher {
}
}
private static boolean checkMultiplexerLaunch(
ProcessControl processControl, UUID request, TerminalLaunchConfiguration config) throws Exception {
if (!config.isPreferTabs()) {
return false;
}
private static String getTerminalRegisterCommand(UUID request) {
var exec = XPipeInstallation.getLocalDefaultCliExecutable();
return "\"" + exec + "\" terminal-register --request " + request;
}
private static boolean launchMultiplexerTabInExistingTerminal(UUID request, TerminalInitScriptConfig initScriptConfig, TerminalLaunchConfiguration launchConfiguration) throws Exception {
var multiplexer = TerminalMultiplexerManager.getEffectiveMultiplexer();
if (multiplexer.isEmpty()) {
return false;
@ -183,52 +221,92 @@ public class TerminalLauncher {
// Throw if not supported
multiplexer.get().checkSupported(TerminalProxyManager.getProxy().orElse(LocalShell.getShell()));
if (!TerminalMultiplexerManager.requiresNewTerminalSession(request)) {
var control = TerminalProxyManager.getProxy();
if (control.isPresent()) {
var type = AppPrefs.get().terminalType().getValue();
var title = type.useColoredTitle() ? config.getColoredTitle() : config.getCleanTitle();
var openCommand = processControl.prepareTerminalOpen(
TerminalInitScriptConfig.ofName(title), WorkingDirectoryFunction.none());
var fullCommand = multiplexer
.get()
.launchScriptExternal(control.get(), openCommand, TerminalInitScriptConfig.ofName(title))
.toString();
control.get().command(fullCommand).execute();
return true;
}
if (TerminalMultiplexerManager.requiresNewTerminalSession(request)) {
return false;
}
return false;
var control = TerminalProxyManager.getProxy();
if (control.isEmpty()) {
return false;
}
var openCommand = launchConfiguration.getDialectLaunchCommand().buildFull(control.get());
var multiplexerCommand = multiplexer
.get()
.launchScriptExternal(control.get(), openCommand, initScriptConfig)
.toString();
control.get().command(multiplexerCommand).execute();
return true;
}
public static String createLaunchCommand(
ProcessControl processControl, TerminalInitScriptConfig config, WorkingDirectoryFunction wd)
throws Exception {
var initScript = AppPrefs.get().terminalInitScript().getValue();
var initialCommand = initScript != null ? initScript.toString() : "";
var openCommand = processControl.prepareTerminalOpen(config, wd);
var proxy = TerminalProxyManager.getProxy();
private static Optional<TerminalLaunchConfiguration> launchMultiplexerTabInNewTerminal(UUID request, TerminalInitScriptConfig initScriptConfig, TerminalLaunchConfiguration launchConfiguration) throws Exception {
var multiplexer = TerminalMultiplexerManager.getEffectiveMultiplexer();
var fullCommand = initialCommand + "\n"
+ (multiplexer.isPresent()
? multiplexer
.get()
.launchScriptSession(
proxy.isPresent() ? proxy.get() : LocalShell.getShell(), openCommand, config)
.toString()
: openCommand);
if (proxy.isPresent()) {
var proxyOpenCommand = fullCommand;
var proxyLaunchCommand = proxy.get()
if (multiplexer.isEmpty()) {
return Optional.empty();
}
// Throw if not supported
multiplexer.get().checkSupported(TerminalProxyManager.getProxy().orElse(LocalShell.getShell()));
if (!TerminalMultiplexerManager.requiresNewTerminalSession(request)) {
return Optional.empty();
}
var initScript = AppPrefs.get().terminalInitScript().getValue();
var initialCommand = initScript != null ? initScript + "\n" : "";
var openCommand = launchConfiguration.getDialectLaunchCommand().buildSimple();
var fullCommand = initialCommand + openCommand;
var proxyControl = TerminalProxyManager.getProxy();
if (proxyControl.isPresent()) {
var proxyMultiplexerCommand = multiplexer
.get()
.launchScriptSession(proxyControl.get(), fullCommand, initScriptConfig)
.toString();
var proxyLaunchCommand = proxyControl.get()
.prepareIntermediateTerminalOpen(
TerminalInitFunction.fixed(proxyOpenCommand),
TerminalInitFunction.fixed(proxyMultiplexerCommand),
TerminalInitScriptConfig.ofName("XPipe"),
WorkingDirectoryFunction.none());
// Restart for the next time
proxy.get().start();
return proxyLaunchCommand;
proxyControl.get().start();
var fullLocalCommand = getTerminalRegisterCommand(request) + "\n" + proxyLaunchCommand;
return Optional.of(new TerminalLaunchConfiguration(null, "XPipe", "XPipe", false, fullLocalCommand, ProcessControlProvider.get()
.getEffectiveLocalDialect()));
} else {
return fullCommand;
var multiplexerCommand = multiplexer
.get()
.launchScriptSession(LocalShell.getShell(), fullCommand, initScriptConfig)
.toString();
var launchCommand = LocalShell.getShell()
.prepareIntermediateTerminalOpen(
TerminalInitFunction.fixed(multiplexerCommand),
TerminalInitScriptConfig.ofName("XPipe"),
WorkingDirectoryFunction.none());
var fullLocalCommand = getTerminalRegisterCommand(request) + "\n" + launchCommand;
return Optional.of(new TerminalLaunchConfiguration(null, "XPipe", "XPipe", false, fullLocalCommand, ProcessControlProvider.get()
.getEffectiveLocalDialect()));
}
}
private static Optional<TerminalLaunchConfiguration> launchProxy(UUID request, TerminalInitScriptConfig initScriptConfig, TerminalLaunchConfiguration launchConfiguration) throws Exception {
var proxyControl = TerminalProxyManager.getProxy();
if (proxyControl.isEmpty()) {
return Optional.empty();
}
var initScript = AppPrefs.get().terminalInitScript().getValue();
var initialCommand = initScript != null ? initScript + "\n" : "";
var openCommand = launchConfiguration.getDialectLaunchCommand().buildSimple();
var fullCommand = initialCommand + openCommand;
var launchCommand = proxyControl.get()
.prepareIntermediateTerminalOpen(
TerminalInitFunction.fixed(fullCommand),
TerminalInitScriptConfig.ofName("XPipe"),
WorkingDirectoryFunction.none());
// Restart for the next time
proxyControl.get().start();
var fullLocalCommand = getTerminalRegisterCommand(request) + "\n" + launchCommand;
return Optional.ofNullable(launchConfiguration.withScript(ProcessControlProvider.get().getEffectiveLocalDialect(), fullLocalCommand));
}
}

View file

@ -85,10 +85,10 @@ public class TerminalLauncherManager {
throw new BeaconClientException("Unable to find terminal child process " + pid);
}
var shell = byPid.get().parent().orElseThrow();
if (req.getPid() != -1 && shell.pid() != req.getPid()) {
if (req.getShellPid() != -1 && shell.pid() != req.getShellPid()) {
throw new BeaconClientException("Wrong launch context");
}
req.setPid(shell.pid());
req.setShellPid(shell.pid());
}
public static void waitExchange(UUID request) throws BeaconClientException, BeaconServerException {

View file

@ -6,7 +6,7 @@ import java.util.*;
public class TerminalMultiplexerManager {
private static final Set<UUID> connectionHubRequests = new HashSet<>();
private static final Map<UUID, TerminalMultiplexer> connectionHubRequests = new HashMap<>();
public static Optional<TerminalMultiplexer> getEffectiveMultiplexer() {
var multiplexer = AppPrefs.get().terminalMultiplexer().getValue();
@ -14,15 +14,16 @@ public class TerminalMultiplexerManager {
}
public static boolean requiresNewTerminalSession(UUID requestUuid) {
if (getEffectiveMultiplexer().isEmpty()) {
connectionHubRequests.add(requestUuid);
var mult = getEffectiveMultiplexer();
if (mult.isEmpty()) {
connectionHubRequests.put(requestUuid, null);
return true;
}
var hasTerminal = TerminalView.get().getSessions().stream()
.anyMatch(shellSession -> shellSession.getTerminal().isRunning()
&& connectionHubRequests.contains(shellSession.getRequest()));
connectionHubRequests.add(requestUuid);
&& mult.get() == connectionHubRequests.get(shellSession.getRequest()));
connectionHubRequests.put(requestUuid, mult.get());
return !hasTerminal;
}
}

View file

@ -48,7 +48,7 @@ public class TerminalProxyManager {
}
var id = ref.get().getProvider().getId();
return List.of("cygwin", "wsl").contains(id);
return id.equals("wsl");
}
public static Optional<ShellControl> getProxy() {

View file

@ -149,14 +149,7 @@ public class TerminalView {
return Optional.empty();
}
// Adjust for terminal logging script setup
var off = trackableTerminalType.getProcessHierarchyOffset();
if (AppPrefs.get().enableTerminalLogging().get() && OsType.getLocal() != OsType.WINDOWS) {
off += 2;
} else if (AppPrefs.get().enableTerminalLogging().get() && OsType.getLocal() == OsType.WINDOWS) {
off += ShellDialects.isPowershell(ProcessControlProvider.get().getEffectiveLocalDialect()) ? 0 : 1;
}
var current = Optional.of(shell);
for (int i = 0; i < 1 + off; i++) {
current = current.flatMap(processHandle -> processHandle.parent());

View file

@ -148,6 +148,7 @@ open module io.xpipe.app {
FsWriteExchangeImpl,
AskpassExchangeImpl,
TerminalPrepareExchangeImpl,
TerminalRegisterExchangeImpl,
TerminalWaitExchangeImpl,
TerminalLaunchExchangeImpl,
TerminalExternalLaunchExchangeImpl,

View file

@ -0,0 +1,32 @@
package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.UUID;
public class TerminalRegisterExchange extends BeaconInterface<TerminalRegisterExchange.Request> {
@Override
public String getPath() {
return "/terminal/register";
}
@Jacksonized
@Builder
@Value
public static class Request {
@NonNull
UUID request;
long pid;
}
@Jacksonized
@Builder
@Value
public static class Response {}
}

View file

@ -47,6 +47,7 @@ open module io.xpipe.beacon {
ConnectionRefreshExchange,
AskpassExchange,
TerminalPrepareExchange,
TerminalRegisterExchange,
TerminalWaitExchange,
TerminalLaunchExchange,
TerminalExternalLaunchExchange,

View file

@ -1 +1 @@
16.0-14
16.0-15