mirror of
https://github.com/xpipe-io/xpipe.git
synced 2025-04-17 09:43:37 +00:00
Rework
This commit is contained in:
parent
041fb45778
commit
466f5d4a75
10 changed files with 140 additions and 80 deletions
|
@ -1,15 +1,18 @@
|
|||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
import io.xpipe.app.ext.ConnectionFileSystem;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.FileBridge;
|
||||
import io.xpipe.app.util.FileOpener;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.ElevationFunction;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileInfo;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -31,19 +34,12 @@ public class BrowserFileOpener {
|
|||
}
|
||||
|
||||
var info = (FileInfo.Unix) file.getInfo();
|
||||
if (info.getPermissions() == null) {
|
||||
var requiresSudo = requiresSudo(model, info, file.getPath());
|
||||
if (!requiresSudo) {
|
||||
return fileSystem.openOutput(file.getPath(), totalBytes);
|
||||
}
|
||||
|
||||
var zero = Integer.valueOf(0);
|
||||
var otherWrite = info.getPermissions().charAt(7) == 'w';
|
||||
var requiresRoot = zero.equals(info.getUid()) && zero.equals(info.getGid()) && !otherWrite;
|
||||
if (!requiresRoot || model.getCache().isRoot()) {
|
||||
return fileSystem.openOutput(file.getPath(), totalBytes);
|
||||
}
|
||||
|
||||
var elevate = AppWindowHelper.showConfirmationAlert(
|
||||
"app.fileWriteSudoTitle", "app.fileWriteSudoHeader", "app.fileWriteSudoContent");
|
||||
var elevate = AppDialog.confirm("fileWriteSudo");
|
||||
if (!elevate) {
|
||||
return fileSystem.openOutput(file.getPath(), totalBytes);
|
||||
}
|
||||
|
@ -66,6 +62,29 @@ public class BrowserFileOpener {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath) throws Exception {
|
||||
if (model.getCache().isRoot()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info != null) {
|
||||
var otherWrite = info.getPermissions().charAt(7) == 'w';
|
||||
if (otherWrite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var userOwned = info.getUid() != null && model.getCache().getUidForUser(model.getCache().getUsername()) == info.getUid() ||
|
||||
info.getUser() != null && model.getCache().getUsername().equals(info.getUser());
|
||||
var userWrite = info.getPermissions().charAt(1) == 'w';
|
||||
if (userOwned && userWrite) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var test = model.getFileSystem().getShell().orElseThrow().command(CommandBuilder.of().add("test", "-w").addFile(filePath)).executeAndCheck();
|
||||
return !test;
|
||||
}
|
||||
|
||||
private static int calculateKey(FileEntry entry) {
|
||||
return Objects.hash(entry.getPath(), entry.getFileSystem(), entry.getKind(), entry.getInfo());
|
||||
}
|
||||
|
|
|
@ -136,7 +136,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
.getAccentColor()
|
||||
.desaturate()
|
||||
.desaturate();
|
||||
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(16, 1, 14, 2)));
|
||||
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(17, 1, 15, 2)));
|
||||
},
|
||||
Platform.getPreferences().accentColorProperty());
|
||||
var hoverBorder = Bindings.createObjectBinding(
|
||||
|
@ -146,7 +146,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
.darker()
|
||||
.desaturate()
|
||||
.desaturate();
|
||||
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(16, 1, 14, 2)));
|
||||
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(17, 1, 15, 2)));
|
||||
},
|
||||
Platform.getPreferences().accentColorProperty());
|
||||
var noneBorder = Bindings.createObjectBinding(
|
||||
|
|
|
@ -21,23 +21,6 @@ public class ContainerStoreState extends ShellStoreState {
|
|||
String containerState;
|
||||
Boolean shellMissing;
|
||||
|
||||
public boolean isExited() {
|
||||
if (containerState == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return containerState.toLowerCase().contains("exited");
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
if (containerState == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return containerState.toLowerCase().contains("running")
|
||||
|| containerState.toLowerCase().contains("up");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStoreState mergeCopy(DataStoreState newer) {
|
||||
var n = (ContainerStoreState) newer;
|
||||
|
|
|
@ -121,7 +121,7 @@ public class TerminalLauncher {
|
|||
TerminalInitFunction.none()),
|
||||
true);
|
||||
var config = new TerminalLaunchConfiguration(null, title, title, true, script, sc.getShellDialect());
|
||||
type.launch(config);
|
||||
launch(type, config, new CountDownLatch(1));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,18 +164,23 @@ public class TerminalLauncher {
|
|||
var config = TerminalLaunchConfiguration.create(
|
||||
request, entry, cleanTitle, adjustedTitle, preferTabs, promptRestart);
|
||||
|
||||
if (preferTabs && launchMultiplexerTabInExistingTerminal(request, terminalConfig, config)) {
|
||||
latch.await();
|
||||
return;
|
||||
}
|
||||
synchronized (TerminalLauncher.class) {
|
||||
// There will be timing issues when launching multiple tabs in a short time span
|
||||
TerminalMultiplexerManager.synchronizeMultiplexerLaunchTiming();
|
||||
|
||||
if (preferTabs) {
|
||||
var multiplexerConfig = launchMultiplexerTabInNewTerminal(request, terminalConfig, config);
|
||||
if (multiplexerConfig.isPresent()) {
|
||||
TerminalMultiplexerManager.registerMultiplexerLaunch(request);
|
||||
launch(type, multiplexerConfig.get(), latch);
|
||||
if (preferTabs && launchMultiplexerTabInExistingTerminal(request, terminalConfig, config)) {
|
||||
latch.await();
|
||||
return;
|
||||
}
|
||||
|
||||
if (preferTabs) {
|
||||
var multiplexerConfig = launchMultiplexerTabInNewTerminal(request, terminalConfig, config);
|
||||
if (multiplexerConfig.isPresent()) {
|
||||
TerminalMultiplexerManager.registerMultiplexerLaunch(request);
|
||||
launch(type, multiplexerConfig.get(), latch);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var proxyConfig = launchProxy(request, config);
|
||||
|
@ -194,6 +199,10 @@ public class TerminalLauncher {
|
|||
|
||||
private static void launch(ExternalTerminalType type, TerminalLaunchConfiguration config, CountDownLatch latch)
|
||||
throws Exception {
|
||||
if (type == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
type.launch(config);
|
||||
latch.await();
|
||||
|
|
|
@ -3,11 +3,14 @@ package io.xpipe.app.terminal;
|
|||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
public class TerminalMultiplexerManager {
|
||||
|
||||
private static UUID pendingMultiplexerLaunch;
|
||||
private static Instant lastCheck = Instant.now();
|
||||
private static final Map<UUID, TerminalMultiplexer> connectionHubRequests = new HashMap<>();
|
||||
|
||||
public static void registerMultiplexerLaunch(UUID uuid) {
|
||||
|
@ -29,7 +32,12 @@ public class TerminalMultiplexerManager {
|
|||
return Optional.ofNullable(multiplexer);
|
||||
}
|
||||
|
||||
public static boolean requiresNewTerminalSession(UUID requestUuid) {
|
||||
public static void synchronizeMultiplexerLaunchTiming() {
|
||||
var mult = getEffectiveMultiplexer();
|
||||
if (mult.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait if we are currently opening a new multiplexer
|
||||
if (pendingMultiplexerLaunch != null) {
|
||||
// Wait for max 10s
|
||||
|
@ -44,6 +52,16 @@ public class TerminalMultiplexerManager {
|
|||
ThreadHelper.sleep(1000);
|
||||
}
|
||||
|
||||
// Synchronize between multiple existing tab launches as well as some multiplexers might break there
|
||||
var elapsed = Duration.between(lastCheck, Instant.now()).toMillis();
|
||||
if (elapsed < 1000) {
|
||||
ThreadHelper.sleep(1000 - elapsed);
|
||||
}
|
||||
|
||||
lastCheck = Instant.now();
|
||||
}
|
||||
|
||||
public static boolean requiresNewTerminalSession(UUID requestUuid) {
|
||||
var mult = getEffectiveMultiplexer();
|
||||
if (mult.isEmpty()) {
|
||||
connectionHubRequests.put(requestUuid, null);
|
||||
|
|
8
dist/changelogs/16.0.md
vendored
8
dist/changelogs/16.0.md
vendored
|
@ -36,6 +36,7 @@ Various improvements were made to the SSH implementation:
|
|||
- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed
|
||||
- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed
|
||||
- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways
|
||||
- There is now the option to enable verbose ssh output to diagnose connection issues better
|
||||
|
||||
## Mini mode
|
||||
|
||||
|
@ -47,6 +48,7 @@ The application window will now hide any unnecessary sidebars when being resized
|
|||
- Various speed improvements for shell operations
|
||||
- Various startup speed improvements
|
||||
- Many dialog windows have now been merged with the main window
|
||||
- The settings menu has been updated to support continuous scrolling
|
||||
- The scripts context menu now shows the respective scripts icons instead of generic ones
|
||||
- Key files synced via git are now synced as pairs if a public key is available
|
||||
- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal
|
||||
|
@ -54,6 +56,10 @@ The application window will now hide any unnecessary sidebars when being resized
|
|||
- The k8s integration will now automatically add all namespaces for the current context when searching for connections
|
||||
- Various error messages now contain a help link to the documentation
|
||||
- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden shutdown
|
||||
- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well
|
||||
- RDP tunnel connections can now be configured with custom authentication and additional RDP options
|
||||
- Improve sudo file write to support more permission cases
|
||||
- The predefined sample script selection has been updated
|
||||
- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization
|
||||
- Upgrade to GraalVM 24
|
||||
|
||||
|
@ -69,3 +75,5 @@ The application window will now hide any unnecessary sidebars when being resized
|
|||
- Fix some file browser terminal dock window ordering issues
|
||||
- Fix repeated file browser errors when remote system did not have the stat command
|
||||
- Fix Windows terminal launch failing if default profile was set to launch as admin
|
||||
- Fix tailscale login check not opening website on Linux
|
||||
- Fix terminal connections failing to launch for some systems with a read-only file system
|
||||
|
|
|
@ -4,14 +4,18 @@ import io.xpipe.app.issue.ErrorEvent;
|
|||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.ScriptHelper;
|
||||
import io.xpipe.app.util.ShellTemp;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.process.ShellDialect;
|
||||
import io.xpipe.core.process.ShellTerminalInitCommand;
|
||||
import io.xpipe.core.process.*;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.util.FailableFunction;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ScriptStoreSetup {
|
||||
|
||||
|
@ -26,6 +30,34 @@ public class ScriptStoreSetup {
|
|||
return;
|
||||
}
|
||||
|
||||
// If we don't have write permissions / it is a read-only file system, don't create scripts
|
||||
if (pc.getOsType() == OsType.LINUX) {
|
||||
var test = pc.command(CommandBuilder.of().add("test", "-w").addFile(pc.getSystemTemporaryDirectory())).executeAndCheck();
|
||||
if (!test) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var checkedPermissions = new AtomicReference<Boolean>();
|
||||
FailableFunction<ShellControl, Boolean, Exception> permissionCheck = (sc) -> {
|
||||
if (checkedPermissions.get() != null) {
|
||||
return checkedPermissions.get();
|
||||
}
|
||||
|
||||
// If we don't have write permissions / it is a read-only file system, don't create scripts
|
||||
if (sc.getOsType() == OsType.LINUX) {
|
||||
var file = sc.getSystemTemporaryDirectory().join("xpipe-test");
|
||||
var test = sc.command(CommandBuilder.of().add("touch").addFile(file).add("&&", "rm").addFile(file)).executeAndCheck();
|
||||
if (!test) {
|
||||
checkedPermissions.set(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
checkedPermissions.set(true);
|
||||
return true;
|
||||
};
|
||||
|
||||
var initFlattened = flatten(enabledScripts).stream()
|
||||
.filter(store -> store.getStore().isInitScript())
|
||||
.toList();
|
||||
|
@ -41,7 +73,11 @@ public class ScriptStoreSetup {
|
|||
initFlattened.forEach(s -> {
|
||||
pc.withInitSnippet(new ShellTerminalInitCommand() {
|
||||
@Override
|
||||
public Optional<String> terminalContent(ShellControl shellControl) {
|
||||
public Optional<String> terminalContent(ShellControl shellControl) throws Exception {
|
||||
if (!permissionCheck.apply(shellControl)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.ofNullable(s.getStore().assembleScriptChain(shellControl));
|
||||
}
|
||||
|
||||
|
@ -58,6 +94,10 @@ public class ScriptStoreSetup {
|
|||
|
||||
@Override
|
||||
public Optional<String> terminalContent(ShellControl shellControl) throws Exception {
|
||||
if (!permissionCheck.apply(shellControl)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (dir == null) {
|
||||
dir = initScriptsDirectory(shellControl, bringFlattened);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import io.xpipe.ext.base.store.StartableStore;
|
|||
import io.xpipe.ext.base.store.StoppableStore;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import io.xpipe.ext.system.incus.IncusContainerStore;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
@ -76,39 +77,31 @@ public class LxdContainerStore
|
|||
@Override
|
||||
public ShellControl control(ShellControl parent) {
|
||||
var user = identity != null ? identity.unwrap().getUsername() : null;
|
||||
var base = new LxdCommandView(parent).exec(containerName, user, () -> {
|
||||
var sc = new LxdCommandView(parent).exec(containerName, user, () -> {
|
||||
var state = getState();
|
||||
var alpine = state.getOsName() != null
|
||||
&& state.getOsName().toLowerCase().contains("alpine");
|
||||
return alpine;
|
||||
});
|
||||
if (identity != null && identity.unwrap().getPassword() != null) {
|
||||
base.setElevationHandler(new BaseElevationHandler(
|
||||
sc.setElevationHandler(new BaseElevationHandler(
|
||||
LxdContainerStore.this, identity.unwrap().getPassword())
|
||||
.orElse(base.getElevationHandler()));
|
||||
.orElse(sc.getElevationHandler()));
|
||||
}
|
||||
return base.withSourceStore(LxdContainerStore.this)
|
||||
.onInit(shellControl -> {
|
||||
var s = getState().toBuilder()
|
||||
.osType(shellControl.getOsType())
|
||||
.shellDialect(shellControl.getShellDialect())
|
||||
.ttyState(shellControl.getTtyState())
|
||||
.running(true)
|
||||
.osName(shellControl.getOsName())
|
||||
.build();
|
||||
setState(s);
|
||||
})
|
||||
.onStartupFail(throwable -> {
|
||||
if (throwable instanceof LicenseRequiredException) {
|
||||
return;
|
||||
}
|
||||
sc.withSourceStore(LxdContainerStore.this);
|
||||
sc.withShellStateInit(LxdContainerStore.this);
|
||||
sc.onStartupFail(throwable -> {
|
||||
if (throwable instanceof LicenseRequiredException) {
|
||||
return;
|
||||
}
|
||||
|
||||
var s = getState().toBuilder()
|
||||
.running(false)
|
||||
.containerState("Connection failed")
|
||||
.build();
|
||||
setState(s);
|
||||
});
|
||||
var s = getState().toBuilder()
|
||||
.running(false)
|
||||
.containerState("Connection failed")
|
||||
.build();
|
||||
setState(s);
|
||||
});
|
||||
return sc;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -140,16 +140,6 @@ public class PodmanContainerStore
|
|||
var pc = new PodmanCommandView(parent).container().exec(containerName);
|
||||
pc.withSourceStore(PodmanContainerStore.this);
|
||||
pc.withShellStateInit(PodmanContainerStore.this);
|
||||
pc.onInit(shellControl -> {
|
||||
var s = getState().toBuilder()
|
||||
.osType(shellControl.getOsType())
|
||||
.shellDialect(shellControl.getShellDialect())
|
||||
.ttyState(shellControl.getTtyState())
|
||||
.running(true)
|
||||
.osName(shellControl.getOsName())
|
||||
.build();
|
||||
setState(s);
|
||||
});
|
||||
pc.onStartupFail(throwable -> {
|
||||
if (throwable instanceof LicenseRequiredException) {
|
||||
return;
|
||||
|
|
4
lang/strings/translations_en.properties
generated
4
lang/strings/translations_en.properties
generated
|
@ -504,8 +504,8 @@ blue=Blue
|
|||
red=Red
|
||||
asktextAlertTitle=Prompt
|
||||
fileWriteSudoTitle=Sudo file write
|
||||
fileWriteSudoHeader=The file you are trying to write requires root privileges. Do you want to write this file with sudo?
|
||||
fileWriteSudoContent=This will automatically elevate to root with either the provided credentials or via a prompt.
|
||||
#force
|
||||
fileWriteSudoContent=The file you are trying to write requires root privileges. Do you want to write this file with sudo? This will automatically elevate to root with either the provided credentials or via a prompt.
|
||||
dontAllowTerminalRestart=Don't allow terminal restart
|
||||
dontAllowTerminalRestartDescription=By default, terminal sessions can be restarted after they ended from within the terminal. To allow this, XPipe will accept these external requests from the terminal to launch the session again\n\nXPipe doesn't have any control over the terminal and where this call comes from, so malicious local applications can use this functionality as well to launch connections through XPipe. Disabling this functionality prevents this scenario.
|
||||
openDocumentation=Open documentation
|
||||
|
|
Loading…
Add table
Reference in a new issue