diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileOpener.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileOpener.java new file mode 100644 index 000000000..2fbaec2bd --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileOpener.java @@ -0,0 +1,83 @@ +package io.xpipe.app.browser; + +import io.xpipe.app.browser.fs.OpenFileSystemModel; +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.store.FileNames; +import io.xpipe.core.store.FileSystem; + +import java.io.OutputStream; + +public class BrowserFileOpener { + + public static void openWithAnyApplication(OpenFileSystemModel model, FileSystem.FileEntry entry) { + var file = entry.getPath(); + var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode(); + FileBridge.get() + .openIO( + FileNames.getFileName(file), + key, + new BooleanScope(model.getBusy()).exclusive(), + () -> { + return entry.getFileSystem().openInput(file); + }, + (size) -> { + if (model.isClosed()) { + return OutputStream.nullOutputStream(); + } + + return entry.getFileSystem().openOutput(file, size); + }, + s -> FileOpener.openWithAnyApplication(s)); + } + + public static void openInDefaultApplication(OpenFileSystemModel model, FileSystem.FileEntry entry) { + var file = entry.getPath(); + var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode(); + FileBridge.get() + .openIO( + FileNames.getFileName(file), + key, + new BooleanScope(model.getBusy()).exclusive(), + () -> { + return entry.getFileSystem().openInput(file); + }, + (size) -> { + if (model.isClosed()) { + return OutputStream.nullOutputStream(); + } + + return entry.getFileSystem().openOutput(file, size); + }, + s -> FileOpener.openInDefaultApplication(s)); + } + + public static void openInTextEditor(OpenFileSystemModel model, FileSystem.FileEntry entry) { + var editor = AppPrefs.get().externalEditor().getValue(); + if (editor == null) { + return; + } + + var file = entry.getPath(); + var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode(); + FileBridge.get() + .openIO( + FileNames.getFileName(file), + key, + new BooleanScope(model.getBusy()).exclusive(), + () -> { + return entry.getFileSystem().openInput(file); + + }, + (size) -> { + if (model.isClosed()) { + return OutputStream.nullOutputStream(); + } + + return entry.getFileSystem().openOutput(file, size); + }, + FileOpener::openInTextEditor); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java index a1fffade1..c9e65f2d8 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java @@ -28,7 +28,7 @@ public interface LeafAction extends BrowserAction { } ThreadHelper.runFailableAsync(() -> { - BooleanScope.execute(model.getBusy(), () -> { + BooleanScope.executeExclusive(model.getBusy(), () -> { // Start shell in case we exited model.getFileSystem().getShell().orElseThrow().start(); execute(model, selected); @@ -77,7 +77,7 @@ public interface LeafAction extends BrowserAction { })); mi.setOnAction(event -> { ThreadHelper.runFailableAsync(() -> { - BooleanScope.execute(model.getBusy(), () -> { + BooleanScope.executeExclusive(model.getBusy(), () -> { // Start shell in case we exited model.getFileSystem().getShell().orElseThrow().start(); execute(model, selected); diff --git a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java index 070c38739..ce47ba26d 100644 --- a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java @@ -79,7 +79,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab { + BooleanScope.executeExclusive(busy, () -> { var fs = entry.getStore().createFileSystem(); if (fs.getShell().isPresent()) { ProcessControlProvider.get().withDefaultScripts(fs.getShell().get()); @@ -100,7 +100,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab { + BooleanScope.executeExclusive(busy, () -> { if (fileSystem == null) { return; } @@ -140,7 +140,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab { + BooleanScope.executeExclusive(busy, () -> { if (entry.getStore() instanceof ShellStore s) { c.accept(fileSystem.getShell().orElseThrow()); if (refresh) { @@ -153,7 +153,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab { + BooleanScope.executeExclusive(busy, () -> { cdSyncWithoutCheck(currentPath.get()); }); } @@ -339,7 +339,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab files) { ThreadHelper.runFailableAsync(() -> { - BooleanScope.execute(busy, () -> { + BooleanScope.executeExclusive(busy, () -> { if (fileSystem == null) { return; } @@ -361,7 +361,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab { - BooleanScope.execute(busy, () -> { + BooleanScope.executeExclusive(busy, () -> { if (fileSystem == null) { return; } @@ -384,7 +384,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab { - BooleanScope.execute(busy, () -> { + BooleanScope.executeExclusive(busy, () -> { if (fileSystem == null) { return; } @@ -408,7 +408,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab { - BooleanScope.execute(busy, () -> { + BooleanScope.executeExclusive(busy, () -> { if (fileSystem == null) { return; } @@ -431,7 +431,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab { - BooleanScope.execute(busy, () -> { + BooleanScope.executeExclusive(busy, () -> { if (fileSystem == null) { return; } @@ -466,7 +466,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab { + BooleanScope.executeExclusive(busy, () -> { if (fileSystem.getShell().isPresent()) { var connection = fileSystem.getShell().get(); var name = (directory != null ? directory + " - " : "") diff --git a/app/src/main/java/io/xpipe/app/util/BooleanScope.java b/app/src/main/java/io/xpipe/app/util/BooleanScope.java index 8cb7e923b..4cf46c197 100644 --- a/app/src/main/java/io/xpipe/app/util/BooleanScope.java +++ b/app/src/main/java/io/xpipe/app/util/BooleanScope.java @@ -1,27 +1,17 @@ package io.xpipe.app.util; -import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.core.util.FailableRunnable; - import javafx.beans.property.BooleanProperty; public class BooleanScope implements AutoCloseable { private final BooleanProperty prop; - private boolean invert; - private boolean forcePlatform; private boolean wait; public BooleanScope(BooleanProperty prop) { this.prop = prop; } - public static void execute(BooleanProperty prop, FailableRunnable r) throws E { - try (var ignored = new BooleanScope(prop).start()) { - r.run(); - } - } - public static void executeExclusive(BooleanProperty prop, FailableRunnable r) throws E { try (var ignored = new BooleanScope(prop).exclusive().start()) { r.run(); @@ -33,37 +23,19 @@ public class BooleanScope implements AutoCloseable { return this; } - public BooleanScope invert() { - this.invert = true; - return this; - } - - public BooleanScope forcePlatform() { - this.forcePlatform = true; - return this; - } - - public BooleanScope start() { + public synchronized BooleanScope start() { if (wait) { - while (!invert == prop.get()) { + while (prop.get()) { ThreadHelper.sleep(50); } } - if (forcePlatform) { - PlatformThread.runLaterIfNeeded(() -> prop.setValue(!invert)); - } else { - prop.setValue(!invert); - } + prop.setValue(true); return this; } @Override - public void close() { - if (forcePlatform) { - PlatformThread.runLaterIfNeeded(() -> prop.setValue(invert)); - } else { - prop.setValue(invert); - } + public synchronized void close() { + prop.setValue(false); } } diff --git a/app/src/main/java/io/xpipe/app/util/FileBridge.java b/app/src/main/java/io/xpipe/app/util/FileBridge.java index 689e0a9f4..fe8e6609e 100644 --- a/app/src/main/java/io/xpipe/app/util/FileBridge.java +++ b/app/src/main/java/io/xpipe/app/util/FileBridge.java @@ -7,16 +7,18 @@ import io.xpipe.app.prefs.AppPrefs; import io.xpipe.core.process.OsType; import io.xpipe.core.util.FailableFunction; import io.xpipe.core.util.FailableSupplier; - import lombok.Getter; import org.apache.commons.io.FileUtils; -import java.io.*; -import java.nio.charset.StandardCharsets; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; +import java.time.Duration; import java.time.Instant; import java.util.*; import java.util.function.BiConsumer; @@ -24,6 +26,37 @@ import java.util.function.Consumer; public class FileBridge { + private static class FixedSizeInputStream extends SimpleFilterInputStream { + + private long count; + private final long size; + + protected FixedSizeInputStream(InputStream in, long size) { + super(in); + this.size = size; + } + + @Override + public int read() throws IOException { + if (count >= size) { + return -1; + } + + var read = in.read(); + count++; + if (read == -1) { + return 0; + } else { + return read; + } + } + + @Override + public int available() throws IOException { + return (int) (size - count); + } + } + private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("bridge"); private static FileBridge INSTANCE; private final Set openEntries = new HashSet<>(); @@ -95,16 +128,17 @@ public class FileBridge { if (e.hasChanged()) { event("Registering change for file " + TEMP.relativize(e.file) + " for editor entry " + e.getName()); e.registerChange(); - var expectedSize = Files.size(e.file); try (var in = Files.newInputStream(e.file)) { var actualSize = (long) in.available(); - if (expectedSize != actualSize) { - event("Expected file size " + expectedSize + " but got size " + actualSize + ". Ignoring change ..."); - return; + var started = Instant.now(); + try (var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), actualSize)) { + e.writer.accept(fixedIn, actualSize); } - - e.writer.accept(in, actualSize); + var taken = Duration.between(started, Instant.now()); + event("Wrote " + HumanReadableFormat.byteCount(actualSize) + " in " + taken.toMillis() + "ms"); } + } else { + event("File doesn't seem to be changed"); } } catch (Exception ex) { ErrorEvent.fromThrowable(ex).omit().handle(); @@ -134,45 +168,10 @@ public class FileBridge { return Optional.empty(); } - public void openReadOnlyString(String input, Consumer fileConsumer) { - if (input == null) { - input = ""; - } - - var id = UUID.randomUUID(); - String s = input; - openIO( - id.toString(), - id, - () -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)), - null, - fileConsumer); - } - - public void openString( - String keyName, Object key, String input, Consumer output, Consumer fileConsumer) { - if (input == null) { - input = ""; - } - - String s = input; - openIO( - keyName, - key, - () -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)), - (size) -> new ByteArrayOutputStream(s.length()) { - @Override - public void close() throws IOException { - super.close(); - output.accept(new String(toByteArray(), StandardCharsets.UTF_8)); - } - }, - fileConsumer); - } - public synchronized void openIO( String keyName, Object key, + BooleanScope scope, FailableSupplier input, FailableFunction output, Consumer consumer) { @@ -206,12 +205,22 @@ public class FileBridge { return; } - var entry = new Entry(file, key, keyName, (in, size) -> { + var entry = new Entry(file, key, keyName, scope, (in, size) -> { if (output != null) { - try (var out = output.apply(size)) { - in.transferTo(out); - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).handle(); + if (scope != null) { + try (var ignored = scope.start()) { + try (var out = output.apply(size)) { + in.transferTo(out); + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).handle(); + } + } + } else { + try (var out = output.apply(size)) { + in.transferTo(out); + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).handle(); + } } } }); @@ -227,13 +236,15 @@ public class FileBridge { private final Path file; private final Object key; private final String name; + private final BooleanScope scope; private final BiConsumer writer; private Instant lastModified; - public Entry(Path file, Object key, String name, BiConsumer writer) { + public Entry(Path file, Object key, String name, BooleanScope scope, BiConsumer writer) { this.file = file; this.key = key; this.name = name; + this.scope = scope; this.writer = writer; } diff --git a/app/src/main/java/io/xpipe/app/util/FileOpener.java b/app/src/main/java/io/xpipe/app/util/FileOpener.java index 8c5abee91..62390236d 100644 --- a/app/src/main/java/io/xpipe/app/util/FileOpener.java +++ b/app/src/main/java/io/xpipe/app/util/FileOpener.java @@ -5,64 +5,20 @@ import io.xpipe.app.prefs.AppPrefs; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.CommandControl; import io.xpipe.core.process.OsType; -import io.xpipe.core.store.FileNames; -import io.xpipe.core.store.FileSystem; import lombok.SneakyThrows; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.FilterInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.UUID; import java.util.function.Consumer; public class FileOpener { - public static void openWithAnyApplication(FileSystem.FileEntry entry) { - var file = entry.getPath(); - var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode(); - FileBridge.get() - .openIO( - FileNames.getFileName(file), - key, - () -> { - return entry.getFileSystem().openInput(file); - }, - (size) -> entry.getFileSystem().openOutput(file, size), - s -> openWithAnyApplication(s)); - } - - public static void openInDefaultApplication(FileSystem.FileEntry entry) { - var file = entry.getPath(); - var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode(); - FileBridge.get() - .openIO( - FileNames.getFileName(file), - key, - () -> { - return entry.getFileSystem().openInput(file); - }, - (size) -> entry.getFileSystem().openOutput(file, size), - s -> openInDefaultApplication(s)); - } - - public static void openInTextEditor(FileSystem.FileEntry entry) { - var editor = AppPrefs.get().externalEditor().getValue(); - if (editor == null) { - return; - } - - var file = entry.getPath(); - var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode(); - FileBridge.get() - .openIO( - FileNames.getFileName(file), - key, - () -> { - return entry.getFileSystem().openInput(file); - }, - (size) -> entry.getFileSystem().openOutput(file, size), - FileOpener::openInTextEditor); - } - public static void openInTextEditor(String localFile) { var editor = AppPrefs.get().externalEditor().getValue(); if (editor == null) { @@ -119,11 +75,40 @@ public class FileOpener { } public static void openReadOnlyString(String input) { - FileBridge.get().openReadOnlyString(input, s -> openInTextEditor(s)); + if (input == null) { + input = ""; + } + + var id = UUID.randomUUID(); + String s = input; + FileBridge.get().openIO( + id.toString(), + id, + null, + () -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)), + null, + v -> openInTextEditor(v)); } public static void openString(String keyName, Object key, String input, Consumer output) { - FileBridge.get().openString(keyName, key, input, output, file -> openInTextEditor(file)); + if (input == null) { + input = ""; + } + + String s = input; + FileBridge.get().openIO( + keyName, + key, + null, + () -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)), + (size) -> new ByteArrayOutputStream(s.length()) { + @Override + public void close() throws IOException { + super.close(); + output.accept(new String(toByteArray(), StandardCharsets.UTF_8)); + } + }, + file -> openInTextEditor(file)); } public static void openCommandOutput(String keyName, Object key, CommandControl cc) { @@ -131,6 +116,7 @@ public class FileOpener { .openIO( keyName, key, + null, () -> new FilterInputStream(cc.getStdout()) { @Override @SneakyThrows diff --git a/app/src/main/java/io/xpipe/app/util/ScanAlert.java b/app/src/main/java/io/xpipe/app/util/ScanAlert.java index c2b0a5972..ef148f383 100644 --- a/app/src/main/java/io/xpipe/app/util/ScanAlert.java +++ b/app/src/main/java/io/xpipe/app/util/ScanAlert.java @@ -110,7 +110,7 @@ public class ScanAlert { window.close(); }); - BooleanScope.execute(busy, () -> { + BooleanScope.executeExclusive(busy, () -> { entry.get().get().setExpanded(true); var copy = new ArrayList<>(selected); for (var a : copy) { @@ -177,7 +177,7 @@ public class ScanAlert { } ThreadHelper.runFailableAsync(() -> { - BooleanScope.execute(busy, () -> { + BooleanScope.executeExclusive(busy, () -> { if (shellControl != null) { shellControl.close(); shellControl = null; diff --git a/app/src/main/java/io/xpipe/app/util/SimpleFilterInputStream.java b/app/src/main/java/io/xpipe/app/util/SimpleFilterInputStream.java new file mode 100644 index 000000000..ce74a3067 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/SimpleFilterInputStream.java @@ -0,0 +1,30 @@ +package io.xpipe.app.util; + +import lombok.NonNull; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +public abstract class SimpleFilterInputStream extends FilterInputStream { + + protected SimpleFilterInputStream(InputStream in) { + super(in); + } + + @Override + public abstract int read() throws IOException; + + @Override + public int read(byte @NonNull [] b, int off, int len) throws IOException { + for (int i = off; i < off + len; i++) { + var r = (byte) read(); + if (r == -1) { + return i - off == 0 ? -1 : i - off; + } + + b[i] = r; + } + return len; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java index 797dec80e..c5c83b0ab 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java @@ -1,11 +1,11 @@ package io.xpipe.ext.base.browser; +import io.xpipe.app.browser.BrowserFileOpener; import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.core.AppI18n; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.app.util.FileOpener; import io.xpipe.core.store.FileKind; import javafx.beans.value.ObservableValue; @@ -28,7 +28,7 @@ public class EditFileAction implements LeafAction { @Override public void execute(OpenFileSystemModel model, List entries) { for (BrowserEntry entry : entries) { - FileOpener.openInTextEditor(entry.getRawFileEntry()); + BrowserFileOpener.openInTextEditor(model, entry.getRawFileEntry()); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java index 63ff54317..64ddec426 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java @@ -1,10 +1,10 @@ package io.xpipe.ext.base.browser; +import io.xpipe.app.browser.BrowserFileOpener; import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.util.FileOpener; import io.xpipe.core.store.FileKind; import javafx.beans.value.ObservableValue; @@ -22,7 +22,7 @@ public class OpenFileDefaultAction implements LeafAction { @Override public void execute(OpenFileSystemModel model, List entries) { for (var entry : entries) { - FileOpener.openInDefaultApplication(entry.getRawFileEntry()); + BrowserFileOpener.openInDefaultApplication(model, entry.getRawFileEntry()); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java index 865a4c635..c64fa1b7a 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java @@ -1,10 +1,10 @@ package io.xpipe.ext.base.browser; +import io.xpipe.app.browser.BrowserFileOpener; import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.util.FileOpener; import io.xpipe.core.process.OsType; import io.xpipe.core.store.FileKind; @@ -23,7 +23,7 @@ public class OpenFileWithAction implements LeafAction { @Override public void execute(OpenFileSystemModel model, List entries) { var e = entries.getFirst(); - FileOpener.openWithAnyApplication(e.getRawFileEntry()); + BrowserFileOpener.openWithAnyApplication(model, e.getRawFileEntry()); } @Override