Rework states, fixed children, and storage logic [stage]

This commit is contained in:
crschnick
2023-08-05 10:13:17 +00:00
parent 5c6b98fd14
commit b0881a2909
21 changed files with 229 additions and 118 deletions

View File

@@ -10,6 +10,8 @@ import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.StackPane;
public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
@@ -25,34 +27,35 @@ public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
@Override
public CompStructure<StackPane> createBase() {
var compStruc = comp.createStructure();
var r = compStruc.get();
var loading = new RingProgressIndicator(0, false);
loading.setProgress(-1);
var loadingBg = new StackPane(loading);
loadingBg.getStyleClass().add("loading-comp");
loadingBg.getStyleClass().add("modal-pane");
var loadingOverlay = new StackPane(loading);
loadingOverlay.getStyleClass().add("loading-comp");
loadingOverlay.getStyleClass().add("modal-pane");
loadingOverlay.visibleProperty().addListener((observable, oldValue, newValue) -> {
r.setOpacity(newValue ? r.getOpacity() * 0.25 : Math.min(1.0, r.getOpacity() * 4));
});
loadingBg.setVisible(showLoading.getValue());
loadingOverlay.setVisible(showLoading.getValue());
var listener = new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean busy) {
if (!busy) {
// Reduce flickering for consecutive loads
Thread t = new Thread(() -> {
ThreadHelper.runAsync(() -> {
try {
Thread.sleep(50);
} catch (InterruptedException ignored) {
}
if (!showLoading.getValue()) {
Platform.runLater(() -> loadingBg.setVisible(false));
Platform.runLater(() -> loadingOverlay.setVisible(false));
}
});
t.setDaemon(true);
t.setName("loading delay");
t.start();
} else {
ThreadHelper.runAsync(() -> {
try {
@@ -61,7 +64,7 @@ public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
}
if (showLoading.getValue()) {
Platform.runLater(() -> loadingBg.setVisible(true));
Platform.runLater(() -> loadingOverlay.setVisible(true));
}
});
}
@@ -69,12 +72,23 @@ public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
};
PlatformThread.sync(showLoading).addListener(listener);
var r = compStruc.get();
var stack = new StackPane(r, loadingBg);
var stack = new StackPane(r, loadingOverlay);
loading.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
return Math.min(r.getHeight() - 20, 50);
}, r.heightProperty()));
r.backgroundProperty().addListener((observable, oldValue, newValue) -> {
loadingOverlay.setBackground(new Background(new BackgroundFill(
loadingOverlay.getBackground() != null ? loadingOverlay.getBackground().getFills().get(
0
).getFill() : null,
newValue.getFills().get(0).getRadii(),
newValue.getFills().get(0).getInsets())));
});
loading.prefWidthProperty()
.bind(Bindings.createDoubleBinding(
() -> {
return Math.min(r.getHeight() - 20, 50);
},
r.heightProperty()));
loading.prefHeightProperty().bind(loading.prefWidthProperty());
return new SimpleCompStructure<>(stack);

View File

@@ -32,10 +32,11 @@ public class StoreToggleComp extends SimpleComp {
var visible = BindingsHelper.persist(Bindings.createBooleanBinding(
() -> {
return (section.getWrapper().getState().getValue() == DataStoreEntry.State.COMPLETE_AND_VALID
|| section.getWrapper().getState().getValue() == DataStoreEntry.State.VALIDATING)
|| section.getWrapper().getValidating().get())
&& section.getShowDetails().get();
},
section.getWrapper().getState(),
section.getWrapper().getValidating(),
section.getShowDetails()));
var t = new NamedToggleComp(value, AppI18n.observable(nameKey))
.visible(visible)

View File

@@ -83,7 +83,7 @@ public abstract class StoreEntryComp extends SimpleComp {
});
new ContextMenuAugment<>(() -> this.createContextMenu()).augment(new SimpleCompStructure<>(button));
var loading = new LoadingOverlayComp(Comp.of(() -> button), wrapper.getLoading());
var loading = new LoadingOverlayComp(Comp.of(() -> button), wrapper.getValidating());
var region = loading.createRegion();
return region;
}

View File

@@ -26,7 +26,7 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
private final DataStoreEntry entry;
private final Property<Instant> lastAccess;
private final BooleanProperty disabled = new SimpleBooleanProperty();
private final BooleanProperty loading = new SimpleBooleanProperty();
private final BooleanProperty validating = new SimpleBooleanProperty();
private final Property<DataStoreEntry.State> state = new SimpleObjectProperty<>();
private final StringProperty information = new SimpleStringProperty();
private final StringProperty summary = new SimpleStringProperty();
@@ -96,7 +96,7 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
expanded.setValue(entry.isExpanded());
information.setValue(entry.getInformation());
loading.setValue(entry.getState() == DataStoreEntry.State.VALIDATING);
validating.setValue(entry.isValidating());
if (entry.getState().isUsable()) {
try {
summary.setValue(entry.getProvider().toSummaryString(entry.getStore(), 50));

View File

@@ -4,6 +4,7 @@ import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableBooleanValue;
@@ -68,7 +69,7 @@ public class StoreSection implements StorageFilter.Filterable {
}
private static StoreSection create(StoreEntryWrapper e, int depth) {
if (!e.getEntry().getState().isUsable()) {
if (e.getEntry().getState() == DataStoreEntry.State.LOAD_FAILED) {
return new StoreSection(e, FXCollections.observableArrayList(), depth);
}

View File

@@ -14,6 +14,7 @@ import io.xpipe.beacon.exchange.MessageExchanges;
import io.xpipe.beacon.exchange.data.ClientErrorMessage;
import io.xpipe.beacon.exchange.data.ServerErrorMessage;
import io.xpipe.core.util.Deobfuscator;
import io.xpipe.core.util.FailableRunnable;
import io.xpipe.core.util.JacksonMapper;
import java.io.IOException;
@@ -132,12 +133,12 @@ public class AppSocketServer {
if (prov.isEmpty()) {
throw new IllegalArgumentException("Unknown request id: " + req.getClass());
}
AtomicReference<BeaconClient.FailableRunnable<Exception>> post = new AtomicReference<>();
AtomicReference<FailableRunnable<Exception>> post = new AtomicReference<>();
var res = prov.get()
.handleRequest(
new BeaconHandler() {
@Override
public void postResponse(BeaconClient.FailableRunnable<Exception> r) {
public void postResponse(FailableRunnable<Exception> r) {
post.set(r);
}

View File

@@ -6,7 +6,7 @@ import lombok.Singular;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ConcurrentHashMap;
@Builder
@Getter
@@ -37,13 +37,19 @@ public class ErrorEvent {
}
public static ErrorEventBuilder fromThrowable(Throwable t) {
var unreportable = UNREPORTABLE.remove(t);
return builder().throwable(t).reportable(!unreportable).description(ExceptionConverter.convertMessage(t));
if (EVENT_BASES.containsKey(t)) {
return EVENT_BASES.remove(t).description(ExceptionConverter.convertMessage(t));
}
return builder().throwable(t).description(ExceptionConverter.convertMessage(t));
}
public static ErrorEventBuilder fromThrowable(String msg, Throwable t) {
var unreportable = UNREPORTABLE.remove(t);
return builder().throwable(t).reportable(!unreportable).description(msg);
if (EVENT_BASES.containsKey(t)) {
return EVENT_BASES.remove(t).description(msg);
}
return builder().throwable(t).description(msg);
}
public static ErrorEventBuilder fromMessage(String msg) {
@@ -74,12 +80,20 @@ public class ErrorEvent {
return omitted(true);
}
public ErrorEventBuilder unreportable() {
return reportable(false);
}
public ErrorEventBuilder discard() {
return omit().unreportable();
}
public void handle() {
build().handle();
}
}
private static Set<Throwable> UNREPORTABLE = new CopyOnWriteArraySet<>();
private static Map<Throwable, ErrorEventBuilder> EVENT_BASES = new ConcurrentHashMap<>();
public static <T extends Throwable> T unreportableIfEndsWith(T t, String... s) {
return unreportableIf(t, t.getMessage() != null && Arrays.stream(s).anyMatch(string->t.getMessage().toLowerCase(Locale.ROOT).endsWith(string)));
@@ -91,13 +105,13 @@ public class ErrorEvent {
public static <T extends Throwable> T unreportableIf(T t, boolean b) {
if (b) {
UNREPORTABLE.add(t);
EVENT_BASES.put(t, ErrorEvent.fromThrowable(t).unreportable());
}
return t;
}
public static <T extends Throwable> T unreportable(T t) {
UNREPORTABLE.add(t);
EVENT_BASES.put(t, ErrorEvent.fromThrowable(t).unreportable());
return t;
}
}

View File

@@ -7,7 +7,10 @@ import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FixedChildStore;
import io.xpipe.core.store.FixedHierarchyStore;
import io.xpipe.core.util.FailableRunnable;
import javafx.util.Pair;
import lombok.Getter;
import lombok.NonNull;
@@ -69,23 +72,81 @@ public abstract class DataStorage {
return INSTANCE;
}
public synchronized boolean refreshChildren(DataStoreEntry e) {
public boolean refreshChildren(DataStoreEntry e) {
return refreshChildren(e, null);
}
public synchronized boolean refreshChildren(DataStoreEntry e, DataStore newValue) {
if (!(e.getStore() instanceof FixedHierarchyStore)) {
return false;
}
var oldChildren = getStoreChildren(e, false, false);
Map<String, FixedChildStore> newChildren;
try {
deleteChildren(e, true);
var newChildren = ((FixedHierarchyStore) e.getStore()).listChildren();
addStoreEntries(newChildren.entrySet().stream()
.map(stringDataStoreEntry -> DataStoreEntry.createNew(
UUID.randomUUID(), stringDataStoreEntry.getKey(), stringDataStoreEntry.getValue()))
.toArray(DataStoreEntry[]::new));
return newChildren.size() > 0;
newChildren = ((FixedHierarchyStore) (newValue != null ? newValue : e.getStore())).listChildren();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return false;
}
var toRemove = oldChildren.stream()
.filter(entry -> newChildren.entrySet().stream()
.noneMatch(
nc -> nc.getValue().getFixedId() == ((FixedChildStore) entry.getStore()).getFixedId()))
.toList();
var toAdd = newChildren.entrySet().stream()
.filter(entry -> oldChildren.stream()
.noneMatch(oc -> ((FixedChildStore) oc.getStore()).getFixedId()
== entry.getValue().getFixedId()))
.toList();
var toUpdate = oldChildren.stream()
.map(entry -> {
FixedChildStore found = newChildren.values().stream()
.filter(nc -> nc.getFixedId() == ((FixedChildStore) entry.getStore()).getFixedId())
.findFirst()
.orElse(null);
return new Pair<>(entry, found);
})
.filter(en -> en.getValue() != null)
.toList();
if (newValue != null) {
e.setStoreInternal(newValue, false);
}
deleteWithChildren(toRemove.toArray(DataStoreEntry[]::new));
addStoreEntries(toAdd.stream()
.map(stringDataStoreEntry -> DataStoreEntry.createNew(
UUID.randomUUID(), stringDataStoreEntry.getKey(), stringDataStoreEntry.getValue()))
.toArray(DataStoreEntry[]::new));
toUpdate.forEach(entry -> {
propagateUpdate(
() -> {
entry.getKey().setStoreInternal(entry.getValue(), false);
},
entry.getKey());
});
save();
return !newChildren.isEmpty();
}
public synchronized void deleteWithChildren(DataStoreEntry... entries) {
var toDelete = Arrays.stream(entries)
.flatMap(entry -> {
// Reverse to delete deepest children first
var ordered = getStoreChildren(entry, false, true);
Collections.reverse(ordered);
return ordered.stream();
})
.toList();
synchronized (this) {
toDelete.forEach(entry -> entry.finalizeEntry());
this.storeEntries.removeAll(toDelete);
this.listeners.forEach(l -> l.onStoreRemove(toDelete.toArray(DataStoreEntry[]::new)));
}
save();
}
public synchronized void deleteChildren(DataStoreEntry e, boolean deep) {
@@ -117,23 +178,22 @@ public abstract class DataStorage {
}
public synchronized List<DataStoreEntry> getStoreChildren(DataStoreEntry entry, boolean display, boolean deep) {
if (!entry.getState().isUsable()) {
if (entry.getState() == DataStoreEntry.State.LOAD_FAILED) {
return List.of();
}
var children = new ArrayList<>(getStoreEntries().stream()
.filter(other -> {
if (!other.getState().isUsable()) {
if (other.getState() == DataStoreEntry.State.LOAD_FAILED) {
return false;
}
var provider = other.getProvider();
var parent = display
? provider.getDisplayParent(other.getStore())
: provider.getLogicalParent(other.getStore());
return parent != null
&& entry.getStore().getClass().equals(parent.getClass())
&& entry.getStore().equals(parent);
var parent = getParent(other, display);
return parent.isPresent()
&& entry.getStore()
.getClass()
.equals(parent.get().getStore().getClass())
&& entry.getStore().equals(parent.get().getStore());
})
.toList());
@@ -208,7 +268,9 @@ public abstract class DataStorage {
public synchronized DataStoreEntry getStoreEntry(@NonNull DataStore store) {
var entry = storeEntries.stream()
.filter(n -> n.getStore() != null && Objects.equals(store.getClass(), n.getStore().getClass()) && store.equals(n.getStore()))
.filter(n -> n.getStore() != null
&& Objects.equals(store.getClass(), n.getStore().getClass())
&& store.equals(n.getStore()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Store not found"));
return entry;
@@ -220,7 +282,11 @@ public abstract class DataStorage {
for (int i = 1; i < id.getNames().size(); i++) {
var children = getStoreChildren(current.get(), false, false);
int finalI = i;
current = children.stream().filter(dataStoreEntry -> dataStoreEntry.getName().equalsIgnoreCase(id.getNames().get(finalI))).findFirst();
current = children.stream()
.filter(dataStoreEntry -> dataStoreEntry
.getName()
.equalsIgnoreCase(id.getNames().get(finalI)))
.findFirst();
if (current.isEmpty()) {
break;
}
@@ -234,8 +300,13 @@ public abstract class DataStorage {
}
public synchronized Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull DataStore store) {
var entry =
storeEntries.stream().filter(n -> store.equals(n.getStore())).findFirst();
var entry = storeEntries.stream()
.filter(n -> {
return n.getStore() != null
&& store.getClass().equals(n.getStore().getClass())
&& store.equals(n.getStore());
})
.findFirst();
return entry;
}
@@ -251,7 +322,7 @@ public abstract class DataStorage {
try {
entry.setStoreInternal(s, false);
entry.refresh(true);
return DataStorage.get().refreshChildren(entry);
return DataStorage.get().refreshChildren(entry, s);
} catch (Exception e) {
entry.setStoreInternal(old, false);
entry.simpleRefresh();
@@ -260,16 +331,19 @@ public abstract class DataStorage {
}
public void updateEntry(DataStoreEntry entry, DataStoreEntry newEntry) {
newEntry.finalizeEntry();
entry.applyChanges(newEntry);
entry.initializeEntry();
propagateUpdate(
() -> {
newEntry.finalizeEntry();
entry.applyChanges(newEntry);
entry.initializeEntry();
},
entry);
}
public void refreshAsync(DataStoreEntry element, boolean deep) {
ThreadHelper.runAsync(() -> {
try {
element.refresh(deep);
propagateUpdate(element);
propagateUpdate(() -> element.refresh(deep), element);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).reportable(false).handle();
}
@@ -277,21 +351,20 @@ public abstract class DataStorage {
});
}
void propagateUpdate(DataStoreEntry origin) {
getStoreChildren(origin, false, false).forEach(entry -> {
<T extends Throwable> void propagateUpdate(FailableRunnable<T> runnable, DataStoreEntry origin) throws T {
var children = getStoreChildren(origin, false, true);
runnable.run();
children.forEach(entry -> {
entry.simpleRefresh();
propagateUpdate(entry);
});
}
public void addStoreEntry(@NonNull DataStoreEntry e) {
e.getProvider().preAdd(e.getStore());
synchronized (this) {
e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));
this.storeEntries.add(e);
}
propagateUpdate(e);
save();
this.listeners.forEach(l -> l.onStoreAdd(e));
@@ -302,10 +375,8 @@ public abstract class DataStorage {
synchronized (this) {
for (DataStoreEntry e : es) {
e.getProvider().preAdd(e.getStore());
e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));
this.storeEntries.add(e);
propagateUpdate(e);
}
this.listeners.forEach(l -> l.onStoreAdd(es));
for (DataStoreEntry e : es) {
@@ -344,11 +415,14 @@ public abstract class DataStorage {
}
public void deleteStoreEntry(@NonNull DataStoreEntry store) {
store.finalizeEntry();
synchronized (this) {
this.storeEntries.remove(store);
}
propagateUpdate(store);
propagateUpdate(
() -> {
store.finalizeEntry();
synchronized (this) {
this.storeEntries.remove(store);
}
},
store);
save();
this.listeners.forEach(l -> l.onStoreRemove(store));
}

View File

@@ -47,6 +47,9 @@ public class DataStoreEntry extends StorageElement {
@NonFinal
boolean expanded;
@NonFinal
boolean validating;
@NonFinal
DataStoreProvider provider;
@@ -102,13 +105,6 @@ public class DataStoreEntry extends StorageElement {
State state,
Configuration configuration,
boolean expanded) {
// The validation must be stuck if that happens
var stateToUse = state;
if (state == State.VALIDATING) {
stateToUse = State.COMPLETE_BUT_INVALID;
}
var entry = new DataStoreEntry(
directory,
uuid,
@@ -118,7 +114,7 @@ public class DataStoreEntry extends StorageElement {
information,
storeNode,
false,
stateToUse,
state,
configuration,
expanded);
return entry;
@@ -259,9 +255,10 @@ public class DataStoreEntry extends StorageElement {
}
if (complete && deep) {
state = State.VALIDATING;
validating = true;
notifyListeners();
store.validate();
validating = false;
state = State.COMPLETE_AND_VALID;
information = getProvider().queryInformationString(getStore(), 50);
dirty = true;
@@ -280,6 +277,7 @@ public class DataStoreEntry extends StorageElement {
state = stateToUse;
}
} catch (Exception e) {
validating = false;
state = State.COMPLETE_BUT_INVALID;
information = getProvider().queryInvalidInformationString(getStore(), 50);
throw e;
@@ -293,12 +291,12 @@ public class DataStoreEntry extends StorageElement {
public void initializeEntry() {
if (store instanceof ExpandedLifecycleStore lifecycleStore) {
try {
state = State.VALIDATING;
validating = true;
notifyListeners();
lifecycleStore.initializeValidate();
state = State.COMPLETE_AND_VALID;
validating = false;
} catch (Exception e) {
state = State.COMPLETE_BUT_INVALID;
validating = false;
ErrorEvent.fromThrowable(e).handle();
} finally {
notifyListeners();
@@ -310,12 +308,10 @@ public class DataStoreEntry extends StorageElement {
public void finalizeEntry() {
if (store instanceof ExpandedLifecycleStore lifecycleStore) {
try {
state = State.VALIDATING;
validating = true;
notifyListeners();
lifecycleStore.finalizeValidate();
state = State.COMPLETE_AND_VALID;
} catch (Exception e) {
state = State.COMPLETE_BUT_INVALID;
ErrorEvent.fromThrowable(e).handle();
} finally {
notifyListeners();
@@ -379,8 +375,6 @@ public class DataStoreEntry extends StorageElement {
COMPLETE_NOT_VALIDATED(true),
@JsonProperty("completeButInvalid")
COMPLETE_BUT_INVALID(true),
@JsonProperty("validating")
VALIDATING(true),
@JsonProperty("completeAndValid")
COMPLETE_AND_VALID(true);

View File

@@ -33,8 +33,7 @@ public class AppDownloads {
.withRateLimitHandler(RateLimitHandler.FAIL)
.withAuthorizationProvider(AuthorizationProvider.ANONYMOUS)
.build();
repository =
github.getRepository(AppProperties.get().isStaging() ? "xpipe-io/xpipe_staging" : "xpipe-io/xpipe");
repository = github.getRepository(AppProperties.get().isStaging() ? "xpipe-io/xpipe_staging" : "xpipe-io/xpipe");
return repository;
}

View File

@@ -137,7 +137,7 @@ public abstract class UpdateHandler {
try {
return refreshUpdateCheck();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
ErrorEvent.fromThrowable(ex).discard().handle();
return null;
}
}