Final updater fixes

This commit is contained in:
crschnick 2023-04-11 21:20:09 +00:00
parent 86ec53ea56
commit d0117fb78e
27 changed files with 671 additions and 516 deletions

View file

@ -1,4 +1,4 @@
<img src="https://user-images.githubusercontent.com/72509152/213873342-7638e830-8a95-4b5d-ad3e-5a9a0b4bf538.png" alt="drawing" width="300"/> <img src="https://user-images.githubusercontent.com/72509152/213873342-7638e830-8a95-4b5d-ad3e-5a9a0b4bf538.png" alt="drawing" width="250"/>
### A smart connection manager and remote file explorer ### A smart connection manager and remote file explorer

View file

@ -42,8 +42,11 @@ public class AppLayoutComp extends Comp<CompStructure<BorderPane>> {
private List<SideMenuBarComp.Entry> createEntryList() { private List<SideMenuBarComp.Entry> createEntryList() {
var l = new ArrayList<>(List.of( var l = new ArrayList<>(List.of(
new SideMenuBarComp.Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()), new SideMenuBarComp.Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()),
new SideMenuBarComp.Entry(AppI18n.observable("browser"), "mdi2f-file-cabinet", new FileBrowserComp(FileBrowserModel.DEFAULT)), new SideMenuBarComp.Entry(
//new SideMenuBarComp.Entry(AppI18n.observable("data"), "mdsal-dvr", new SourceCollectionLayoutComp()), AppI18n.observable("browser"),
"mdi2f-file-cabinet",
new FileBrowserComp(FileBrowserModel.DEFAULT)),
// new SideMenuBarComp.Entry(AppI18n.observable("data"), "mdsal-dvr", new SourceCollectionLayoutComp()),
new SideMenuBarComp.Entry( new SideMenuBarComp.Entry(
AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new PrefsComp(this)), AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new PrefsComp(this)),
// new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new // new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new
@ -51,9 +54,10 @@ public class AppLayoutComp extends Comp<CompStructure<BorderPane>> {
// new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp()), // new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp()),
new SideMenuBarComp.Entry(AppI18n.observable("about"), "mdi2p-package-variant", new AboutTabComp()))); new SideMenuBarComp.Entry(AppI18n.observable("about"), "mdi2p-package-variant", new AboutTabComp())));
if (AppProperties.get().isDeveloperMode()) { if (AppProperties.get().isDeveloperMode()) {
l.add(new SideMenuBarComp.Entry(AppI18n.observable("developer"), "mdi2b-book-open-variant", new l.add(new SideMenuBarComp.Entry(
DeveloperTabComp())); AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));
} }
// l.add(new SideMenuBarComp.Entry(AppI18n.observable("abc"), "mdi2b-book-open-variant", Comp.of(() -> { // l.add(new SideMenuBarComp.Entry(AppI18n.observable("abc"), "mdi2b-book-open-variant", Comp.of(() -> {
// var fi = new FontIcon("mdsal-dvr"); // var fi = new FontIcon("mdsal-dvr");
// fi.setIconSize(30); // fi.setIconSize(30);

View file

@ -3,13 +3,11 @@ package io.xpipe.app.comp.about;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.update.AppUpdater;
import io.xpipe.app.update.UpdateAvailableAlert; import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -20,62 +18,51 @@ import org.kordamp.ikonli.javafx.FontIcon;
public class UpdateCheckComp extends SimpleComp { public class UpdateCheckComp extends SimpleComp {
private final ObservableBooleanValue updateAvailable;
private final ObservableValue<Boolean> updateReady; private final ObservableValue<Boolean> updateReady;
public UpdateCheckComp() { public UpdateCheckComp() {
updateAvailable = Bindings.createBooleanBinding( updateReady = PlatformThread.sync(Bindings.createBooleanBinding(
() -> { () -> {
return AppUpdater.get().getLastUpdateCheckResult().getValue() != null return XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate().getValue() != null;
&& AppUpdater.get()
.getLastUpdateCheckResult()
.getValue()
.isUpdate();
}, },
PlatformThread.sync(AppUpdater.get().getLastUpdateCheckResult())); XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate()));
updateReady = Bindings.createBooleanBinding(
() -> {
return AppUpdater.get().getDownloadedUpdate().getValue() != null;
},
PlatformThread.sync(AppUpdater.get().getDownloadedUpdate()));
} }
private void restart() { private void restart() {
AppUpdater.get().refreshUpdateCheckSilent(); XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent();
UpdateAvailableAlert.showIfNeeded(); UpdateAvailableAlert.showIfNeeded();
} }
private void download() {
AppUpdater.get().downloadUpdateAsync();
}
private void refresh() { private void refresh() {
AppUpdater.get().checkForUpdateAsync(); ThreadHelper.runFailableAsync(() -> {
XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck();
XPipeDistributionType.get().getUpdateHandler().prepareUpdate();
});
} }
private ObservableValue<String> descriptionText() { private ObservableValue<String> descriptionText() {
return PlatformThread.sync(Bindings.createStringBinding( return PlatformThread.sync(Bindings.createStringBinding(
() -> { () -> {
if (AppUpdater.get().getDownloadedUpdate().getValue() != null) { if (XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate().getValue() != null) {
return AppI18n.get("updateRestart"); return null;
} }
if (AppUpdater.get().getLastUpdateCheckResult().getValue() != null if (XPipeDistributionType.get().getUpdateHandler().getLastUpdateCheckResult().getValue() != null
&& AppUpdater.get() && XPipeDistributionType.get().getUpdateHandler()
.getLastUpdateCheckResult() .getLastUpdateCheckResult()
.getValue() .getValue()
.isUpdate()) { .isUpdate()) {
return AppI18n.get( return AppI18n.get(
"updateAvailable", "updateAvailable",
AppUpdater.get() XPipeDistributionType.get().getUpdateHandler()
.getLastUpdateCheckResult() .getLastUpdateCheckResult()
.getValue() .getValue()
.getVersion()); .getVersion());
} }
if (AppUpdater.get().getLastUpdateCheckResult().getValue() != null) { if (XPipeDistributionType.get().getUpdateHandler().getLastUpdateCheckResult().getValue() != null) {
return AppI18n.readableDuration( return AppI18n.readableDuration(
new SimpleObjectProperty<>(AppUpdater.get() new SimpleObjectProperty<>(XPipeDistributionType.get().getUpdateHandler()
.getLastUpdateCheckResult() .getLastUpdateCheckResult()
.getValue() .getValue()
.getCheckTime()), .getCheckTime()),
@ -85,15 +72,15 @@ public class UpdateCheckComp extends SimpleComp {
return null; return null;
} }
}, },
AppUpdater.get().getLastUpdateCheckResult(), XPipeDistributionType.get().getUpdateHandler().getLastUpdateCheckResult(),
AppUpdater.get().getDownloadedUpdate(), XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate(),
AppUpdater.get().getBusy())); XPipeDistributionType.get().getUpdateHandler().getBusy()));
} }
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var button = new Button(); var button = new Button();
button.disableProperty().bind(PlatformThread.sync(AppUpdater.get().getBusy())); button.disableProperty().bind(PlatformThread.sync(XPipeDistributionType.get().getUpdateHandler().getBusy()));
button.textProperty() button.textProperty()
.bind(Bindings.createStringBinding( .bind(Bindings.createStringBinding(
() -> { () -> {
@ -101,30 +88,18 @@ public class UpdateCheckComp extends SimpleComp {
return AppI18n.get("updateReady"); return AppI18n.get("updateReady");
} }
if (updateAvailable.getValue()) { return AppI18n.get("checkForUpdates");
return XPipeDistributionType.get().supportsUpdate()
? AppI18n.get("downloadUpdate")
: AppI18n.get("checkOutUpdate");
} else {
return AppI18n.get("checkForUpdates");
}
}, },
updateAvailable,
updateReady)); updateReady));
button.graphicProperty() button.graphicProperty()
.bind(Bindings.createObjectBinding( .bind(Bindings.createObjectBinding(
() -> { () -> {
if (updateReady.getValue()) { if (updateReady.getValue()) {
return new FontIcon("mdi2r-restart"); return new FontIcon("mdi2a-apple-airplay");
} }
if (updateAvailable.getValue()) { return new FontIcon("mdi2r-refresh");
return new FontIcon("mdi2d-download");
} else {
return new FontIcon("mdi2r-refresh");
}
}, },
updateAvailable,
updateReady)); updateReady));
button.getStyleClass().add("button-comp"); button.getStyleClass().add("button-comp");
button.setOnAction(e -> { button.setOnAction(e -> {
@ -133,14 +108,7 @@ public class UpdateCheckComp extends SimpleComp {
return; return;
} }
if (updateAvailable.getValue() && !XPipeDistributionType.get().supportsUpdate()) { refresh();
Hyperlinks.open(
AppUpdater.get().getLastUpdateCheckResult().getValue().getReleaseUrl());
} else if (updateAvailable.getValue()) {
download();
} else {
refresh();
}
}); });
var checked = new Label(); var checked = new Label();

View file

@ -1,9 +1,14 @@
package io.xpipe.app.comp.base; package io.xpipe.app.comp.base;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.update.XPipeDistributionType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
@ -40,6 +45,18 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
}); });
vbox.getChildren().add(b.createRegion()); vbox.getChildren().add(b.createRegion());
}); });
{
// vbox.getChildren().add(new Spacer(Orientation.VERTICAL));
var fi = new FontIcon("mdi2u-update");
var b = new BigIconButton(AppI18n.observable("update"), fi, () -> UpdateAvailableAlert.showIfNeeded());
b.apply(GrowAugment.create(true, false));
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(() -> {
return XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate().getValue() == null;
}, XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate())));
vbox.getChildren().add(b.createRegion());
}
vbox.getStyleClass().add("sidebar-comp"); vbox.getStyleClass().add("sidebar-comp");
return new SimpleCompStructure<>(vbox); return new SimpleCompStructure<>(vbox);
} }

View file

@ -5,7 +5,7 @@ import io.xpipe.app.comp.AppLayoutComp;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.update.AppUpdater; import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import javafx.application.Application; import javafx.application.Application;
import javafx.application.Platform; import javafx.application.Platform;
@ -76,21 +76,17 @@ public class App extends Application {
() -> { () -> {
var base = String.format( var base = String.format(
"X-Pipe Desktop (%s)", AppProperties.get().getVersion()); "X-Pipe Desktop (%s)", AppProperties.get().getVersion());
var suffix = AppUpdater.get().getLastUpdateCheckResult().getValue() != null var suffix = XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate().getValue() != null
&& AppUpdater.get()
.getLastUpdateCheckResult()
.getValue()
.isUpdate()
? String.format( ? String.format(
" (Update to %s available)", " (Update to %s ready)",
AppUpdater.get() XPipeDistributionType.get().getUpdateHandler()
.getLastUpdateCheckResult() .getPreparedUpdate()
.getValue() .getValue()
.getVersion()) .getVersion())
: ""; : "";
return base + suffix; return base + suffix;
}, },
AppUpdater.get().getLastUpdateCheckResult()); XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate());
var appWindow = new AppMainWindow(stage); var appWindow = new AppMainWindow(stage);
appWindow.getStage().titleProperty().bind(PlatformThread.sync(titleBinding)); appWindow.getStage().titleProperty().bind(PlatformThread.sync(titleBinding));

View file

@ -3,10 +3,12 @@ package io.xpipe.app.core.mode;
import io.xpipe.app.comp.storage.collection.SourceCollectionViewState; import io.xpipe.app.comp.storage.collection.SourceCollectionViewState;
import io.xpipe.app.comp.storage.store.StoreViewState; import io.xpipe.app.comp.storage.store.StoreViewState;
import io.xpipe.app.core.*; import io.xpipe.app.core.*;
import io.xpipe.app.issue.*; import io.xpipe.app.issue.ErrorAction;
import io.xpipe.app.issue.ErrorHandler;
import io.xpipe.app.issue.LogErrorHandler;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.update.AppUpdater;
import io.xpipe.app.util.FileBridge; import io.xpipe.app.util.FileBridge;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
@ -40,7 +42,6 @@ public class BaseMode extends OperationMode {
AppFileWatcher.init(); AppFileWatcher.init();
FileBridge.init(); FileBridge.init();
AppSocketServer.init(); AppSocketServer.init();
AppUpdater.init();
TrackEvent.info("mode", "Finished base components initialization"); TrackEvent.info("mode", "Finished base components initialization");
} }

View file

@ -11,7 +11,7 @@ import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.launcher.LauncherCommand; import io.xpipe.app.launcher.LauncherCommand;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.util.XPipeDaemonMode; import io.xpipe.core.util.XPipeDaemonMode;
import io.xpipe.core.util.XPipeSession; import io.xpipe.app.util.XPipeSession;
import org.apache.commons.lang3.function.FailableRunnable; import org.apache.commons.lang3.function.FailableRunnable;
import java.util.ArrayList; import java.util.ArrayList;

View file

@ -68,7 +68,7 @@ public class ErrorDetailsComp extends SimpleComp {
// AppI18n.observable("events"), "mdi2c-clipboard-list-outline", createTrackEventHistory())); // AppI18n.observable("events"), "mdi2c-clipboard-list-outline", createTrackEventHistory()));
// } // }
var tb = new TabPaneComp(new SimpleObjectProperty<>(items.get(0)), items); var tb = new TabPaneComp(new SimpleObjectProperty<>(items.size() > 0 ? items.get(0) : null), items);
tb.apply(r -> AppFont.small(r.get())); tb.apply(r -> AppFont.small(r.get()));
return tb.createRegion(); return tb.createRegion();
} }

View file

@ -6,7 +6,7 @@ import io.sentry.protocol.User;
import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppProperties;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import java.nio.file.Files; import java.nio.file.Files;
@ -23,7 +23,7 @@ public class SentryErrorHandler {
options.setEnableUncaughtExceptionHandler(false); options.setEnableUncaughtExceptionHandler(false);
options.setAttachServerName(false); options.setAttachServerName(false);
// options.setDebug(true); // options.setDebug(true);
options.setDist(XPipeDistributionType.get().getName()); options.setDist(XPipeDistributionType.get().getId());
options.setRelease(AppProperties.get().getVersion()); options.setRelease(AppProperties.get().getVersion());
options.setEnableShutdownHook(false); options.setEnableShutdownHook(false);
options.setProguardUuid(AppProperties.get().getBuildUuid().toString()); options.setProguardUuid(AppProperties.get().getBuildUuid().toString());

View file

@ -3,8 +3,8 @@ package io.xpipe.app.issue;
import io.sentry.Sentry; import io.sentry.Sentry;
import io.xpipe.app.core.*; import io.xpipe.app.core.*;
import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.update.AppUpdater;
import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.update.XPipeDistributionType;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonBar;
@ -87,8 +87,7 @@ public class TerminalErrorHandler implements ErrorHandler {
private static void handleProbableUpdate() { private static void handleProbableUpdate() {
try { try {
AppUpdater.init(); var rel = XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck();
var rel = AppUpdater.get().refreshUpdateCheck();
if (rel != null && rel.isUpdate()) { if (rel != null && rel.isUpdate()) {
var update = AppWindowHelper.showBlockingAlert(alert -> { var update = AppWindowHelper.showBlockingAlert(alert -> {
alert.setAlertType(Alert.AlertType.INFORMATION); alert.setAlertType(Alert.AlertType.INFORMATION);

View file

@ -14,7 +14,6 @@ import io.xpipe.app.ext.PrefsHandler;
import io.xpipe.app.ext.PrefsProvider; import io.xpipe.app.ext.PrefsProvider;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.XPipeDistributionType;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableBooleanValue;
@ -146,13 +145,14 @@ public class AppPrefs {
// Automatically update // Automatically update
// ==================== // ====================
private final BooleanProperty automaticallyUpdate = private final BooleanProperty automaticallyCheckForUpdates =
typed(new SimpleBooleanProperty(XPipeDistributionType.get().supportsUpdate()), Boolean.class); typed(new SimpleBooleanProperty(true), Boolean.class);
private final BooleanField automaticallyUpdateField = private final BooleanField automaticallyCheckForUpdatesField =
BooleanField.ofBooleanType(automaticallyUpdate).render(() -> new CustomToggleControl()); BooleanField.ofBooleanType(automaticallyCheckForUpdates).render(() -> new CustomToggleControl());
private final BooleanProperty updateToPrereleases = typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField updateToPrereleasesField = private final BooleanProperty checkForPrereleases = typed(new SimpleBooleanProperty(false), Boolean.class);
BooleanField.ofBooleanType(updateToPrereleases).render(() -> new CustomToggleControl()); private final BooleanField checkForPrereleasesField =
BooleanField.ofBooleanType(checkForPrereleases).render(() -> new CustomToggleControl());
private final BooleanProperty confirmDeletions = typed(new SimpleBooleanProperty(true), Boolean.class); private final BooleanProperty confirmDeletions = typed(new SimpleBooleanProperty(true), Boolean.class);
@ -236,11 +236,11 @@ public class AppPrefs {
} }
public ReadOnlyBooleanProperty automaticallyUpdate() { public ReadOnlyBooleanProperty automaticallyUpdate() {
return automaticallyUpdate; return automaticallyCheckForUpdates;
} }
public ReadOnlyBooleanProperty updateToPrereleases() { public ReadOnlyBooleanProperty updateToPrereleases() {
return updateToPrereleases; return checkForPrereleases;
} }
public ReadOnlyBooleanProperty confirmDeletions() { public ReadOnlyBooleanProperty confirmDeletions() {
@ -431,12 +431,8 @@ public class AppPrefs {
Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)), Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)),
Group.of( Group.of(
"updates", "updates",
Setting.of("automaticallyUpdate", automaticallyUpdateField, automaticallyUpdate) Setting.of("automaticallyUpdate", automaticallyCheckForUpdatesField, automaticallyCheckForUpdates),
.applyVisibility(VisibilityProperty.of(new SimpleBooleanProperty( Setting.of("updateToPrereleases", checkForPrereleasesField, checkForPrereleases)),
XPipeDistributionType.get().supportsUpdate()))),
Setting.of("updateToPrereleases", updateToPrereleasesField, updateToPrereleases)
.applyVisibility(VisibilityProperty.of(new SimpleBooleanProperty(
XPipeDistributionType.get().supportsUpdate())))),
Group.of( Group.of(
"advanced", "advanced",
Setting.of("storageDirectory", storageDirectoryControl, internalStorageDirectory), Setting.of("storageDirectory", storageDirectoryControl, internalStorageDirectory),

View file

@ -8,6 +8,7 @@ import io.xpipe.core.impl.FileNames;
import io.xpipe.core.impl.LocalStore; import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;
import lombok.Getter; import lombok.Getter;
import java.util.List; import java.util.List;
@ -64,7 +65,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
new SimpleType("gnomeTerminal", "gnome-terminal", "Gnome Terminal") { new SimpleType("gnomeTerminal", "gnome-terminal", "Gnome Terminal") {
@Override @Override
public void launch(String name, String file) throws Exception { public void launch(String name, String file, boolean elevated) throws Exception {
try (ShellControl pc = LocalStore.getShell()) { try (ShellControl pc = LocalStore.getShell()) {
ApplicationHelper.checkSupport(pc, executable, getDisplayName()); ApplicationHelper.checkSupport(pc, executable, getDisplayName());
@ -144,7 +145,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.orElse(null); .orElse(null);
} }
public abstract void launch(String name, String file) throws Exception; public abstract void launch(String name, String file, boolean elevated) throws Exception;
static class MacOsTerminalType extends ExternalApplicationType.MacApplication implements ExternalTerminalType { static class MacOsTerminalType extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
@ -153,7 +154,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
@Override @Override
public void launch(String name, String file) throws Exception { public void launch(String name, String file, boolean elevated) throws Exception {
try (ShellControl pc = LocalStore.getShell()) { try (ShellControl pc = LocalStore.getShell()) {
var suffix = file.equals(pc.getShellDialect().getOpenCommand()) var suffix = file.equals(pc.getShellDialect().getOpenCommand())
? "\"\"" ? "\"\""
@ -171,7 +172,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
@Override @Override
public void launch(String name, String file) throws Exception { public void launch(String name, String file, boolean elevated) throws Exception {
var custom = AppPrefs.get().customTerminalCommand().getValue(); var custom = AppPrefs.get().customTerminalCommand().getValue();
if (custom == null || custom.isBlank()) { if (custom == null || custom.isBlank()) {
throw new IllegalStateException("No custom terminal command specified"); throw new IllegalStateException("No custom terminal command specified");
@ -207,7 +208,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
@Override @Override
public void launch(String name, String file) throws Exception { public void launch(String name, String file, boolean elevated) throws Exception {
try (ShellControl pc = LocalStore.getShell()) { try (ShellControl pc = LocalStore.getShell()) {
var cmd = String.format( var cmd = String.format(
""" """
@ -241,7 +242,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
@Override @Override
public void launch(String name, String file) throws Exception { public void launch(String name, String file, boolean elevated) throws Exception {
if (!MacOsPermissions.waitForAccessibilityPermissions()) { if (!MacOsPermissions.waitForAccessibilityPermissions()) {
return; return;
} }
@ -279,7 +280,18 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
@Override @Override
public void launch(String name, String file) throws Exception { public void launch(String name, String file, boolean elevated) throws Exception {
if (elevated) {
if (OsType.getLocal().equals(OsType.WINDOWS)) {
try (ShellControl pc = LocalStore.getShell().subShell(ShellDialects.POWERSHELL).start()) {
ApplicationHelper.checkSupport(pc, executable, displayName);
var toExecute = "Start-Process \"" + executable + "\" -Verb RunAs -ArgumentList \"" + toCommand(name, file).replaceAll("\"", "`\"") + "\"";
pc.executeSimpleCommand(toExecute);
}
return;
}
}
try (ShellControl pc = LocalStore.getShell()) { try (ShellControl pc = LocalStore.getShell()) {
ApplicationHelper.checkSupport(pc, executable, displayName); ApplicationHelper.checkSupport(pc, executable, displayName);

View file

@ -2,7 +2,7 @@ package io.xpipe.app.storage;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.util.XPipeSession; import io.xpipe.app.util.XPipeSession;
import lombok.NonNull; import lombok.NonNull;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;

View file

@ -6,7 +6,7 @@ import io.xpipe.beacon.BeaconDaemonController;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
import io.xpipe.core.util.XPipeDaemonMode; import io.xpipe.core.util.XPipeDaemonMode;
import io.xpipe.core.util.XPipeSession; import io.xpipe.app.util.XPipeSession;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;

View file

@ -2,7 +2,7 @@ package io.xpipe.app.test;
import io.xpipe.app.ext.XPipeServiceProviders; import io.xpipe.app.ext.XPipeServiceProviders;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
import io.xpipe.core.util.XPipeSession; import io.xpipe.app.util.XPipeSession;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import java.util.UUID; import java.util.UUID;

View file

@ -1,308 +0,0 @@
package io.xpipe.app.update;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.XPipeDistributionType;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import lombok.Builder;
import lombok.Getter;
import lombok.Value;
import lombok.With;
import lombok.extern.jackson.Jacksonized;
import org.kohsuke.github.GHRelease;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
@Getter
public class AppUpdater {
private static AppUpdater INSTANCE;
private final Property<AvailableRelease> lastUpdateCheckResult = new SimpleObjectProperty<>();
private final Property<DownloadedUpdate> downloadedUpdate = new SimpleObjectProperty<>();
private final BooleanProperty busy = new SimpleBooleanProperty();
private final PerformedUpdate performedUpdate;
private final boolean updateSucceeded;
private AppUpdater() {
performedUpdate = AppCache.get("performedUpdate", PerformedUpdate.class, () -> null);
var hasUpdated = performedUpdate != null;
event("Was updated is " + hasUpdated);
if (hasUpdated) {
AppCache.clear("performedUpdate");
updateSucceeded = AppProperties.get().getVersion().equals(performedUpdate.getNewVersion());
AppCache.clear("lastUpdateCheckResult");
AppCache.clear("downloadedUpdate");
event("Found information about recent update");
} else {
updateSucceeded = false;
}
downloadedUpdate.setValue(AppCache.get("downloadedUpdate", DownloadedUpdate.class, () -> null));
// Check if the original version this was downloaded from is still the same
if (downloadedUpdate.getValue() != null
&& !downloadedUpdate
.getValue()
.getSourceVersion()
.equals(AppProperties.get().getVersion())) {
downloadedUpdate.setValue(null);
}
// Check if somehow the downloaded version is equal to the current one
if (downloadedUpdate.getValue() != null
&& downloadedUpdate
.getValue()
.getVersion()
.equals(AppProperties.get().getVersion())) {
downloadedUpdate.setValue(null);
}
if (!XPipeDistributionType.get().supportsUpdate()) {
downloadedUpdate.setValue(null);
}
downloadedUpdate.addListener((c, o, n) -> {
AppCache.update("downloadedUpdate", n);
});
lastUpdateCheckResult.addListener((c, o, n) -> {
if (n != null && downloadedUpdate.getValue() != null && n.isUpdate() && n.getVersion().equals(downloadedUpdate.getValue().getVersion())) {
return;
}
downloadedUpdate.setValue(null);
});
if (XPipeDistributionType.get().checkForUpdateOnStartup()) {
refreshUpdateCheckSilent();
}
}
private static void event(String msg) {
TrackEvent.builder().category("installer").type("info").message(msg).handle();
}
public static AppUpdater get() {
return INSTANCE;
}
public static void init() {
if (INSTANCE != null) {
return;
}
INSTANCE = new AppUpdater();
startBackgroundUpdater();
}
private static void startBackgroundUpdater() {
if (XPipeDistributionType.get().supportsUpdate()
&& XPipeDistributionType.get() != XPipeDistributionType.DEVELOPMENT) {
ThreadHelper.create("updater", true, () -> {
ThreadHelper.sleep(Duration.ofMinutes(10).toMillis());
event("Starting background updater thread");
while (true) {
var rel = INSTANCE.refreshUpdateCheckSilent();
if (rel != null
&& AppPrefs.get().automaticallyUpdate().get() && rel.isUpdate()) {
event("Performing background update");
INSTANCE.downloadUpdate();
}
ThreadHelper.sleep(Duration.ofHours(1).toMillis());
}
})
.start();
}
}
private static boolean isUpdate(String releaseVersion) {
if (AppPrefs.get() != null
&& AppPrefs.get().developerMode().getValue()
&& AppPrefs.get().developerDisableUpdateVersionCheck().get()) {
event("Bypassing version check");
return true;
}
if (!AppProperties.get().getVersion().equals(releaseVersion)) {
event("Release has a different version");
return true;
}
return false;
}
public void downloadUpdateAsync() {
ThreadHelper.runAsync(() -> downloadUpdate());
}
public synchronized void downloadUpdate() {
if (busy.getValue()) {
return;
}
if (lastUpdateCheckResult.getValue() == null) {
return;
}
if (!XPipeDistributionType.get().supportsUpdate()) {
return;
}
if (!lastUpdateCheckResult.getValue().isUpdate()) {
return;
}
try (var ignored = new BusyProperty(busy)) {
event("Performing update download ...");
try {
var downloadFile = AppDownloads.downloadInstaller(
lastUpdateCheckResult.getValue().getAssetType(),
lastUpdateCheckResult.getValue().version,
false);
if (downloadFile.isEmpty()) {
return;
}
var changelogString = AppDownloads.downloadChangelog(lastUpdateCheckResult.getValue().version, false);
var changelog = changelogString.orElse(null);
var rel = new DownloadedUpdate(
AppProperties.get().getVersion(),
lastUpdateCheckResult.getValue().version,
downloadFile.get(),
changelog,
lastUpdateCheckResult.getValue().getAssetType());
downloadedUpdate.setValue(rel);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
}
}
}
public void executeUpdateAndClose() {
if (busy.getValue()) {
return;
}
if (downloadedUpdate.getValue() == null) {
return;
}
var downloadFile = downloadedUpdate.getValue().getFile();
if (!Files.exists(downloadFile)) {
return;
}
event("Executing update ...");
OperationMode.executeAfterShutdown(() -> {
try {
AppInstaller.installFileLocal(downloadedUpdate.getValue().getAssetType(), downloadFile);
} catch (Throwable ex) {
ex.printStackTrace();
} finally {
var performedUpdate = new PerformedUpdate(
downloadedUpdate.getValue().getVersion(),
downloadedUpdate.getValue().getBody(),
downloadedUpdate.getValue().getVersion());
AppCache.update("performedUpdate", performedUpdate);
}
});
}
public void checkForUpdateAsync() {
ThreadHelper.runAsync(() -> refreshUpdateCheckSilent());
}
public synchronized AvailableRelease refreshUpdateCheckSilent() {
try {
return refreshUpdateCheck();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
return null;
}
}
public synchronized AvailableRelease refreshUpdateCheck() throws IOException {
if (busy.getValue()) {
return lastUpdateCheckResult.getValue();
}
try (var ignored = new BusyProperty(busy)) {
var rel = AppDownloads.getLatestSuitableRelease();
event("Determined latest suitable release "
+ rel.map(GHRelease::getName).orElse(null));
if (rel.isEmpty()) {
lastUpdateCheckResult.setValue(null);
return null;
}
var isUpdate = isUpdate(rel.get().getTagName());
var assetType = AppInstaller.getSuitablePlatformAsset();
var ghAsset = rel.orElseThrow().listAssets().toList().stream()
.filter(g -> assetType.isCorrectAsset(g.getName()))
.findAny();
if (ghAsset.isEmpty()) {
return null;
}
event("Selected asset " + ghAsset.get().getName());
lastUpdateCheckResult.setValue(new AvailableRelease(
AppProperties.get().getVersion(),
rel.get().getTagName(),
rel.get().getHtmlUrl().toString(),
ghAsset.get().getBrowserDownloadUrl(),
assetType,
Instant.now(),
isUpdate));
}
return lastUpdateCheckResult.getValue();
}
@Value
@Builder
@Jacksonized
public static class PerformedUpdate {
String name;
String rawDescription;
String newVersion;
}
@Value
@Builder
@Jacksonized
@With
public static class AvailableRelease {
String sourceVersion;
String version;
String releaseUrl;
String downloadUrl;
AppInstaller.InstallerAssetType assetType;
Instant checkTime;
boolean isUpdate;
}
@Value
@Builder
@Jacksonized
public static class DownloadedUpdate {
String sourceVersion;
String version;
Path file;
String body;
AppInstaller.InstallerAssetType assetType;
}
}

View file

@ -0,0 +1,50 @@
package io.xpipe.app.update;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.fxcomps.impl.CodeSnippet;
import io.xpipe.app.fxcomps.impl.CodeSnippetComp;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.layout.Region;
import java.time.Instant;
public class ChocoUpdater extends UpdateHandler {
public ChocoUpdater() {
super(true);
}
@Override
public Region createInterface() {
var snippet = CodeSnippet.builder()
.keyword("choco")
.space()
.identifier("install")
.space()
.string("xpipe")
.space()
.keyword("--version=" + getPreparedUpdate().getValue().getVersion())
.build();
return new CodeSnippetComp(false, new SimpleObjectProperty<>(snippet)).createRegion();
}
public AvailableRelease refreshUpdateCheckImpl() throws Exception {
try (var sc = ShellStore.createLocal().create().start()) {
var latest = sc.executeStringSimpleCommand(
"choco outdated -r --nocolor").lines().filter(s -> s.startsWith("xpipe")).findAny().orElseThrow().split("\\|")[2];
var isUpdate = isUpdate(latest);
var rel = new AvailableRelease(
AppProperties.get().getVersion(),
XPipeDistributionType.get().getId(),
latest,
"https://community.chocolatey.org/packages/xpipe/" + latest,
null,
null,
Instant.now(),
isUpdate);
lastUpdateCheckResult.setValue(rel);
return lastUpdateCheckResult.getValue();
}
}
}

View file

@ -0,0 +1,84 @@
package io.xpipe.app.update;
import io.xpipe.app.core.AppProperties;
import javafx.scene.layout.Region;
import org.kohsuke.github.GHRelease;
import java.nio.file.Files;
import java.time.Instant;
public class GitHubUpdater extends UpdateHandler {
public GitHubUpdater(boolean startBackgroundThread) {
super(startBackgroundThread);
}
@Override
public Region createInterface() {
return null;
}
public void prepareUpdateImpl() {
var downloadFile = AppDownloads.downloadInstaller(
lastUpdateCheckResult.getValue().getAssetType(),
lastUpdateCheckResult.getValue().getVersion(),
false);
if (downloadFile.isEmpty()) {
return;
}
var changelogString = AppDownloads.downloadChangelog(
lastUpdateCheckResult.getValue().getVersion(), false);
var changelog = changelogString.orElse(null);
var rel = new PreparedUpdate(
AppProperties.get().getVersion(),
XPipeDistributionType.get().getId(),
lastUpdateCheckResult.getValue().getVersion(),
lastUpdateCheckResult.getValue().getReleaseUrl(),
downloadFile.get(),
changelog,
lastUpdateCheckResult.getValue().getAssetType());
preparedUpdate.setValue(rel);
}
public void executeUpdateAndCloseImpl() throws Exception {
var downloadFile = preparedUpdate.getValue().getFile();
if (!Files.exists(downloadFile)) {
return;
}
AppInstaller.installFileLocal(preparedUpdate.getValue().getAssetType(), downloadFile);
}
public synchronized AvailableRelease refreshUpdateCheckImpl() throws Exception {
var rel = AppDownloads.getLatestSuitableRelease();
event("Determined latest suitable release "
+ rel.map(GHRelease::getName).orElse(null));
if (rel.isEmpty()) {
lastUpdateCheckResult.setValue(null);
return null;
}
var isUpdate = isUpdate(rel.get().getTagName());
var assetType = AppInstaller.getSuitablePlatformAsset();
var ghAsset = rel.orElseThrow().listAssets().toList().stream()
.filter(g -> assetType.isCorrectAsset(g.getName()))
.findAny();
if (ghAsset.isEmpty()) {
return null;
}
event("Selected asset " + ghAsset.get().getName());
lastUpdateCheckResult.setValue(new AvailableRelease(
AppProperties.get().getVersion(),
XPipeDistributionType.get().getId(),
rel.get().getTagName(),
rel.get().getHtmlUrl().toString(),
ghAsset.get().getBrowserDownloadUrl(),
assetType,
Instant.now(),
isUpdate));
return lastUpdateCheckResult.getValue();
}
}

View file

@ -0,0 +1,55 @@
package io.xpipe.app.update;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.util.Hyperlinks;
import javafx.scene.layout.Region;
import org.kohsuke.github.GHRelease;
import java.time.Instant;
public class PortableUpdater extends UpdateHandler {
public PortableUpdater() {
super(true);
}
@Override
public Region createInterface() {
return new ButtonComp(AppI18n.observable("checkOutUpdate"), () -> {
Hyperlinks.open(XPipeDistributionType.get()
.getUpdateHandler()
.getPreparedUpdate()
.getValue()
.getReleaseUrl());
}).createRegion();
}
public void executeUpdateAndCloseImpl() throws Exception {
throw new UnsupportedOperationException();
}
public synchronized AvailableRelease refreshUpdateCheckImpl() throws Exception {
var rel = AppDownloads.getLatestSuitableRelease();
event("Determined latest suitable release "
+ rel.map(GHRelease::getName).orElse(null));
if (rel.isEmpty()) {
lastUpdateCheckResult.setValue(null);
return null;
}
var isUpdate = isUpdate(rel.get().getTagName());
lastUpdateCheckResult.setValue(new AvailableRelease(
AppProperties.get().getVersion(),
XPipeDistributionType.get().getId(),
rel.get().getTagName(),
rel.get().getHtmlUrl().toString(),
null,
null,
Instant.now(),
isUpdate));
return lastUpdateCheckResult.getValue();
}
}

View file

@ -3,42 +3,51 @@ package io.xpipe.app.update;
import io.xpipe.app.comp.base.MarkdownComp; import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.core.AppWindowHelper;
import javafx.geometry.Insets;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
public class UpdateAvailableAlert { public class UpdateAvailableAlert {
public static void showIfNeeded() { public static void showIfNeeded() {
if (AppUpdater.get().getDownloadedUpdate().getValue() == null) { UpdateHandler uh = XPipeDistributionType.get().getUpdateHandler();
if (uh.getPreparedUpdate().getValue() == null) {
return; return;
} }
var u = AppUpdater.get().getDownloadedUpdate().getValue(); var u = uh.getPreparedUpdate().getValue();
var update = AppWindowHelper.showBlockingAlert(alert -> { var update = AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("updateReadyAlertTitle")); alert.setTitle(AppI18n.get("updateReadyAlertTitle"));
alert.setAlertType(Alert.AlertType.NONE); alert.setAlertType(Alert.AlertType.NONE);
if (u.getBody() != null && !u.getBody().isBlank()) { var markdown = new MarkdownComp(u.getBody() != null ? u.getBody() : "", s -> {
var markdown = new MarkdownComp(u.getBody(), s -> { var header = "<h1>" + AppI18n.get("whatsNew", u.getVersion()) + "</h1>";
var header = "<h1>" + AppI18n.get("whatsNew", u.getVersion()) + "</h1>"; return header + s;
return header + s; })
}) .createRegion();
.createRegion(); alert.getButtonTypes().clear();
alert.getDialogPane().setContent(markdown); var updaterContent = uh.createInterface();
if (updaterContent != null) {
var stack = new StackPane(updaterContent);
stack.setPadding(new Insets(18));
var box = new VBox(markdown, stack);
box.setFillWidth(true);
box.setPadding(Insets.EMPTY);
alert.getDialogPane().setContent(box);
} else { } else {
alert.getDialogPane() alert.getDialogPane().setContent(markdown);
.setContent(AppWindowHelper.alertContentText(AppI18n.get("updateReadyAlertContent"))); alert.getButtonTypes().add(new ButtonType(AppI18n.get("install"), ButtonBar.ButtonData.OK_DONE));
} }
alert.getButtonTypes().clear();
alert.getButtonTypes().add(new ButtonType(AppI18n.get("install"), ButtonBar.ButtonData.OK_DONE));
alert.getButtonTypes().add(new ButtonType(AppI18n.get("ignore"), ButtonBar.ButtonData.NO)); alert.getButtonTypes().add(new ButtonType(AppI18n.get("ignore"), ButtonBar.ButtonData.NO));
}) })
.map(buttonType -> buttonType.getButtonData().isDefaultButton()) .map(buttonType -> buttonType.getButtonData().isDefaultButton())
.orElse(false); .orElse(false);
if (update) { if (update) {
AppUpdater.get().executeUpdateAndClose(); uh.executeUpdateAndClose();
} }
} }
} }

View file

@ -12,9 +12,8 @@ import javafx.stage.Modality;
public class UpdateChangelogAlert { public class UpdateChangelogAlert {
public static void showIfNeeded() { public static void showIfNeeded() {
var update = AppUpdater.get().getPerformedUpdate(); var update = XPipeDistributionType.get().getUpdateHandler().getPerformedUpdate();
if (update != null && !XPipeDistributionType.get().getUpdateHandler().isUpdateSucceeded()) {
if (update != null && !AppUpdater.get().isUpdateSucceeded()) {
ErrorEvent.fromMessage("Update did not succeed").handle(); ErrorEvent.fromMessage("Update did not succeed").handle();
return; return;
} }

View file

@ -0,0 +1,264 @@
package io.xpipe.app.update;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.layout.Region;
import lombok.Builder;
import lombok.Getter;
import lombok.Value;
import lombok.With;
import lombok.extern.jackson.Jacksonized;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
@Getter
public abstract class UpdateHandler {
protected final Property<AvailableRelease> lastUpdateCheckResult = new SimpleObjectProperty<>();
protected final Property<PreparedUpdate> preparedUpdate = new SimpleObjectProperty<>();
protected final BooleanProperty busy = new SimpleBooleanProperty();
protected final PerformedUpdate performedUpdate;
protected final boolean updateSucceeded;
protected UpdateHandler(boolean startBackgroundThread) {
performedUpdate = AppCache.get("performedUpdate", PerformedUpdate.class, () -> null);
var hasUpdated = performedUpdate != null;
event("Was updated is " + hasUpdated);
if (hasUpdated) {
AppCache.clear("performedUpdate");
updateSucceeded = AppProperties.get().getVersion().equals(performedUpdate.getNewVersion());
AppCache.clear("preparedUpdate");
event("Found information about recent update");
} else {
updateSucceeded = false;
}
preparedUpdate.setValue(AppCache.get("preparedUpdate", PreparedUpdate.class, () -> null));
// Check if the original version this was downloaded from is still the same
if (preparedUpdate.getValue() != null
&& (!preparedUpdate
.getValue()
.getSourceVersion()
.equals(AppProperties.get().getVersion())
|| !XPipeDistributionType.get()
.getId()
.equals(preparedUpdate.getValue().getSourceDist()))) {
preparedUpdate.setValue(null);
}
// Check if somehow the downloaded version is equal to the current one
if (preparedUpdate.getValue() != null
&& preparedUpdate
.getValue()
.getVersion()
.equals(AppProperties.get().getVersion())) {
preparedUpdate.setValue(null);
}
preparedUpdate.addListener((c, o, n) -> {
AppCache.update("preparedUpdate", n);
});
lastUpdateCheckResult.addListener((c, o, n) -> {
if (n != null
&& preparedUpdate.getValue() != null
&& n.isUpdate()
&& n.getVersion().equals(preparedUpdate.getValue().getVersion())) {
return;
}
preparedUpdate.setValue(null);
});
if (startBackgroundThread) {
startBackgroundUpdater();
}
}
private void startBackgroundUpdater() {
ThreadHelper.create("updater", true, () -> {
ThreadHelper.sleep(Duration.ofMinutes(5).toMillis());
event("Starting background updater thread");
while (true) {
if (AppPrefs.get().automaticallyUpdate().get()) {
event("Performing background update");
refreshUpdateCheckSilent();
prepareUpdate();
}
ThreadHelper.sleep(Duration.ofHours(1).toMillis());
}
})
.start();
}
protected void event(String msg) {
TrackEvent.builder().category("updater").type("info").message(msg).handle();
}
protected final boolean isUpdate(String releaseVersion) {
if (AppPrefs.get() != null
&& AppPrefs.get().developerMode().getValue()
&& AppPrefs.get().developerDisableUpdateVersionCheck().get()) {
event("Bypassing version check");
return true;
}
if (!AppProperties.get().getVersion().equals(releaseVersion)) {
event("Release has a different version");
return true;
}
return false;
}
public final void prepareUpdateAsync() {
ThreadHelper.runAsync(() -> prepareUpdate());
}
public final void refreshUpdateCheckAsync() {
ThreadHelper.runAsync(() -> refreshUpdateCheckSilent());
}
public final AvailableRelease refreshUpdateCheckSilent() {
try {
return refreshUpdateCheck();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
return null;
}
}
public final void prepareUpdate() {
if (busy.getValue()) {
return;
}
if (lastUpdateCheckResult.getValue() == null) {
return;
}
if (!lastUpdateCheckResult.getValue().isUpdate()) {
return;
}
try (var ignored = new BusyProperty(busy)) {
event("Performing update download ...");
prepareUpdateImpl();
}
}
public abstract Region createInterface();
public void prepareUpdateImpl() {
var changelogString =
AppDownloads.downloadChangelog(lastUpdateCheckResult.getValue().getVersion(), false);
var changelog = changelogString.orElse(null);
var rel = new PreparedUpdate(
AppProperties.get().getVersion(),
XPipeDistributionType.get().getId(),
lastUpdateCheckResult.getValue().getVersion(),
lastUpdateCheckResult.getValue().getReleaseUrl(),
null,
changelog,
lastUpdateCheckResult.getValue().getAssetType());
preparedUpdate.setValue(rel);
}
public final void executeUpdateAndClose() {
if (busy.getValue()) {
return;
}
if (preparedUpdate.getValue() == null) {
return;
}
var downloadFile = preparedUpdate.getValue().getFile();
if (!Files.exists(downloadFile)) {
return;
}
event("Executing update ...");
OperationMode.executeAfterShutdown(() -> {
try {
executeUpdateAndCloseImpl();
} catch (Throwable ex) {
ex.printStackTrace();
} finally {
var performedUpdate = new PerformedUpdate(
preparedUpdate.getValue().getVersion(),
preparedUpdate.getValue().getBody(),
preparedUpdate.getValue().getVersion());
AppCache.update("performedUpdate", performedUpdate);
}
});
}
public void executeUpdateAndCloseImpl() throws Exception {
throw new UnsupportedOperationException();
}
public final AvailableRelease refreshUpdateCheck() throws Exception {
if (busy.getValue()) {
return lastUpdateCheckResult.getValue();
}
try (var ignored = new BusyProperty(busy)) {
return refreshUpdateCheckImpl();
}
}
public abstract AvailableRelease refreshUpdateCheckImpl() throws Exception;
@Value
@Builder
@Jacksonized
public static class PerformedUpdate {
String name;
String rawDescription;
String newVersion;
}
@Value
@Builder
@Jacksonized
@With
public static class AvailableRelease {
String sourceVersion;
String sourceDist;
String version;
String releaseUrl;
String downloadUrl;
AppInstaller.InstallerAssetType assetType;
Instant checkTime;
boolean isUpdate;
}
@Value
@Builder
@Jacksonized
public static class PreparedUpdate {
String sourceVersion;
String sourceDist;
String version;
String releaseUrl;
Path file;
String body;
AppInstaller.InstallerAssetType assetType;
}
}

View file

@ -0,0 +1,77 @@
package io.xpipe.app.update;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.XPipeSession;
import io.xpipe.core.impl.LocalStore;
import lombok.Getter;
import java.util.Arrays;
import java.util.function.Supplier;
public enum XPipeDistributionType {
DEVELOPMENT("development", () -> new GitHubUpdater(false)),
PORTABLE("portable", () -> new PortableUpdater()),
INSTALLATION("install", () -> new GitHubUpdater(true)),
CHOCO("choco", () -> new ChocoUpdater());
private static XPipeDistributionType type;
XPipeDistributionType(String id, Supplier<UpdateHandler> updateHandlerSupplier) {
this.id = id;
this.updateHandlerSupplier = updateHandlerSupplier;
}
public static XPipeDistributionType get() {
if (type != null) {
return type;
}
if (!XPipeSession.get().isNewBuildSession()) {
var cached = AppCache.get("dist", String.class, () -> null);
var cachedType = Arrays.stream(values())
.filter(xPipeDistributionType ->
xPipeDistributionType.getId().equals(cached))
.findAny()
.orElse(null);
if (cachedType != null) {
return (type = cachedType);
}
}
type = determine();
AppCache.update("dist", type.getId());
return type;
}
public static XPipeDistributionType determine() {
try (var sc = LocalStore.getShell()) {
try (var chocoOut = sc.command("choco search --local-only -r xpipe").start()) {
var out = chocoOut.readStdoutDiscardErr();
if (chocoOut.getExitCode() == 0) {
var split = out.split("\\|");
if (split.length == 2) {
return CHOCO;
}
}
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
return XPipeDistributionType.INSTALLATION;
}
@Getter
private final String id;
private UpdateHandler updateHandler;
private final Supplier<UpdateHandler> updateHandlerSupplier;
public UpdateHandler getUpdateHandler() {
if (updateHandler == null) {
updateHandler = updateHandlerSupplier.get();
}
return updateHandler;
}
}

View file

@ -21,6 +21,6 @@ public class TerminalHelper {
throw new IllegalStateException(AppI18n.get("noTerminalSet")); throw new IllegalStateException(AppI18n.get("noTerminalSet"));
} }
type.launch(title, command); type.launch(title, command, false);
} }
} }

View file

@ -1,77 +0,0 @@
package io.xpipe.app.util;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.XPipeInstallation;
public interface XPipeDistributionType {
XPipeDistributionType DEVELOPMENT = new XPipeDistributionType() {
@Override
public boolean checkForUpdateOnStartup() {
return false;
}
@Override
public boolean supportsUpdate() {
return true;
}
@Override
public String getName() {
return "development";
}
};
XPipeDistributionType PORTABLE = new XPipeDistributionType() {
@Override
public boolean checkForUpdateOnStartup() {
return false;
}
@Override
public boolean supportsUpdate() {
return false;
}
@Override
public String getName() {
return "portable";
}
};
XPipeDistributionType INSTALLATION = new XPipeDistributionType() {
@Override
public boolean checkForUpdateOnStartup() {
return true;
}
@Override
public boolean supportsUpdate() {
return true;
}
@Override
public String getName() {
return "install";
}
};
static XPipeDistributionType get() {
if (!ModuleHelper.isImage()) {
return DEVELOPMENT;
}
if (XPipeInstallation.isInstallationDistribution()) {
return INSTALLATION;
} else {
return PORTABLE;
}
}
boolean checkForUpdateOnStartup();
boolean supportsUpdate();
String getName();
}

View file

@ -1,6 +1,9 @@
package io.xpipe.core.util; package io.xpipe.app.util;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppProperties;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.util.UuidHelper;
import lombok.Value; import lombok.Value;
import java.nio.file.Files; import java.nio.file.Files;
@ -13,6 +16,8 @@ public class XPipeSession {
boolean isNewSystemSession; boolean isNewSystemSession;
boolean isNewBuildSession;
/** /**
* Unique identifier that resets on every X-Pipe restart. * Unique identifier that resets on every X-Pipe restart.
*/ */
@ -61,7 +66,11 @@ public class XPipeSession {
} catch (Exception ignored) { } catch (Exception ignored) {
} }
INSTANCE = new XPipeSession(isNewSystemSession, UUID.randomUUID(), buildSessionId, systemSessionId); var s = AppCache.get("lastBuild", String.class, () -> buildSessionId.toString());
var isBuildChanged = !buildSessionId.toString().equals(s);
AppCache.update("lastBuild", AppProperties.get().getVersion());
INSTANCE = new XPipeSession(isNewSystemSession, isBuildChanged, UUID.randomUUID(), buildSessionId, systemSessionId);
} }
public static XPipeSession get() { public static XPipeSession get() {

View file

@ -16,7 +16,7 @@ saveWindowLocation=Save window location
saveWindowLocationDescription=Controls whether the window coordinates should be saved and restored on restarts. saveWindowLocationDescription=Controls whether the window coordinates should be saved and restored on restarts.
startupShutdown=Startup / Shutdown startupShutdown=Startup / Shutdown
system=System system=System
updateToPrereleases=Update to prereleases updateToPrereleases=Include prereleases
updateToPrereleasesDescription=When enabled, the update check will also look for available prereleases in addition to full releases. updateToPrereleasesDescription=When enabled, the update check will also look for available prereleases in addition to full releases.
storage=Storage storage=Storage
runOnStartup=Run on startup runOnStartup=Run on startup
@ -45,8 +45,8 @@ cancel=Cancel
notAnAbsolutePath=Not an absolute path notAnAbsolutePath=Not an absolute path
notADirectory=Not a directory notADirectory=Not a directory
notAnEmptyDirectory=Not an empty directory notAnEmptyDirectory=Not an empty directory
automaticallyUpdate=Automatically update automaticallyUpdate=Check for updates
automaticallyUpdateDescription=When enabled, new releases are automatically downloaded in the background while X-Pipe is running and installed on the next launch. automaticallyUpdateDescription=When enabled, new releases are automatically fetched in the background while X-Pipe is running.
sendAnonymousErrorReports=Send anonymous error reports sendAnonymousErrorReports=Send anonymous error reports
sendUsageStatistics=Send anonymous usage statistics sendUsageStatistics=Send anonymous usage statistics
storageDirectory=Storage directory storageDirectory=Storage directory