Rework RPCDevice to be more intuitive for typical cases.
unmount() is now called in addition to when suspend() was called before. dispose() is called after unmount() if cleanup is required (machine stopped, device removed).
This commit is contained in:
@@ -97,8 +97,6 @@ minecraft {
|
||||
source sourceSets.main
|
||||
}
|
||||
}
|
||||
|
||||
arg "-mixin.config=oc2.mixins.json"
|
||||
}
|
||||
|
||||
client {
|
||||
@@ -119,6 +117,10 @@ minecraft {
|
||||
|
||||
mixin {
|
||||
add sourceSets.main, "oc2.refmap.json"
|
||||
|
||||
config 'oc2.mixins.json'
|
||||
|
||||
quiet
|
||||
}
|
||||
|
||||
task copyGeneratedResources(type: Copy) {
|
||||
|
||||
@@ -11,23 +11,20 @@ import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
*/
|
||||
public interface LifecycleAwareDevice {
|
||||
/**
|
||||
* This method corresponds to {@link RPCDevice#mount()}. It is called when the device is initialized, either
|
||||
* because its virtual machine starts running, or because it is added to a running virtual machine.
|
||||
* This method corresponds to {@link RPCDevice#mount()}.
|
||||
*/
|
||||
default void onDeviceMounted() {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method corresponds to {@link RPCDevice#unmount()}. It is called when the device is disposed, either
|
||||
* because its virtual machine stops running, or because it is removed from a running virtual machine.
|
||||
* This method corresponds to {@link RPCDevice#unmount()}.
|
||||
*/
|
||||
default void onDeviceUnmounted() {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method corresponds to {@link RPCDevice#suspend()}. It is called when its virtual machine is suspended,
|
||||
* either due to the containing chunk being unloaded, or the containing world being unloaded.
|
||||
* This method corresponds to {@link RPCDevice#dispose()}.
|
||||
*/
|
||||
default void onDeviceSuspended() {
|
||||
default void onDeviceDisposed() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,9 @@ public final class ObjectDevice implements RPCDevice {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void suspend() {
|
||||
public void dispose() {
|
||||
if (object instanceof LifecycleAwareDevice device) {
|
||||
device.onDeviceSuspended();
|
||||
device.onDeviceDisposed();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,35 +20,27 @@ import java.util.List;
|
||||
* <p>
|
||||
* The lifecycle for {@link RPCDevice}s is as follows:
|
||||
* <pre>
|
||||
* ┌──────────────────────────────────┐
|
||||
* │VirtualMachine.isRunning() = false◄──────────────────────┐
|
||||
* └────────────────┬─────────────────┘ │
|
||||
* │ │
|
||||
* ┌──────────▼───────────┐ │
|
||||
* │VirtualMachine.start()│ │
|
||||
* └──────────┬───────────┘ │
|
||||
* │ │
|
||||
* │ ┌──────────┐ │
|
||||
* │ │Chunk Load│ ┌───────────────────┐ │
|
||||
* ├───┼──────────◄───┤RPCDevice.suspend()│ │
|
||||
* │ │World Load│ └──────▲────────────┘ │
|
||||
* │ └──────────┘ │ │
|
||||
* │ │ │
|
||||
* ┌────────▼────────┐ ┌─────┴──────┐ │
|
||||
* ┌──────────►RPCDevice.mount()│ │Chunk Unload│ │
|
||||
* │ └────────┬────────┘ ┌─►────────────┤ │
|
||||
* │ │ │ │World Unload│ │
|
||||
* │ ┌─────────────────▼───────────────┐ │ └────────────┘ │
|
||||
* │ │VirtualMachine.isRunning() = true├─┤ │
|
||||
* │ └─────┬───────────────────┬───────┘ │ ┌──────────────────┐ │
|
||||
* │ │ │ │ │Computer Shutdown │ │
|
||||
* │ ┌─────▼──────┐ ┌──────▼───────┐ └─►──────────────────┤ │
|
||||
* └─┤Device Added│ │Device Removed│ │Computer Destroyed│ │
|
||||
* └────────────┘ └──────┬───────┘ └─────┬────────────┘ │
|
||||
* │ │ │
|
||||
* ┌──────────▼────────┐ ┌──────▼────────────┐ │
|
||||
* │RPCDevice.unmount()│ │RPCDevice.unmount()├─┘
|
||||
* └───────────────────┘ └───────────────────┘
|
||||
* ┌──────────────┐ ┌────────────────┐
|
||||
* │serializeNBT()│ │deserializeNBT()◄─────────────────┐
|
||||
* └──────────────┘ └───────┬────────┘ │
|
||||
* May be called │VM starts or │
|
||||
* at any time, │resumes after │
|
||||
* except while │load │
|
||||
* unloaded... ┌───▼───┐ │
|
||||
* │mount()│ │
|
||||
* └───┬───┘ │
|
||||
* │VM stops or │
|
||||
* │is unloaded │
|
||||
* │ │
|
||||
* ┌────▼────┐Chunk unloaded │
|
||||
* │unmount()├─────────────────────┤
|
||||
* └────┬────┘ │
|
||||
* │VM stopped or │
|
||||
* │device removed │
|
||||
* │ │
|
||||
* ┌────▼────┐ │
|
||||
* │dispose()├─────────────────────┘
|
||||
* └─────────┘
|
||||
* </pre>
|
||||
*
|
||||
* @see ObjectDevice
|
||||
@@ -78,7 +70,7 @@ public interface RPCDevice extends Device {
|
||||
List<RPCMethodGroup> getMethodGroups();
|
||||
|
||||
/**
|
||||
* Called to initialize this device.
|
||||
* Called to start this device.
|
||||
* <p>
|
||||
* This is called when the connected virtual machine starts, or when the device is added to an already running
|
||||
* virtual machine.
|
||||
@@ -87,22 +79,28 @@ public interface RPCDevice extends Device {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to dispose this device.
|
||||
* Called to stop this device.
|
||||
* <p>
|
||||
* Called when the connected virtual machine stops, or when the device is removed from a currently running
|
||||
* virtual machine.
|
||||
* Called when the connected virtual machine stops, the device is removed from a currently running
|
||||
* virtual machine, or the connected virtual machine is suspended (chunk unload/server stopped/...).
|
||||
* <p>
|
||||
* If {@link #mount()} was called, this is guaranteed to be called.
|
||||
*/
|
||||
default void unmount() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the device is suspended.
|
||||
* Called to dispose this device.
|
||||
* <p>
|
||||
* This can happen when the level area containing the context the device was loaded in is unloaded,
|
||||
* e.g. due to player moving too far away from the area.
|
||||
* Called when the connected virtual machine stops or the device is removed from a currently running
|
||||
* virtual machine.
|
||||
* <p>
|
||||
* Intended for soft-releasing unmanaged resource, i.e. non-persisted unmanaged resources.
|
||||
* Will only be called on unmounted devices (i.e. will always be called after {@link #unmount()} if
|
||||
* {@link #mount()} was called before). May be called without intermediary {@link #mount()} calls, e.g.
|
||||
* virtual machine stops, then device is disconnected from the virtual machine.
|
||||
* <p>
|
||||
* Intended for releasing persistent unmanaged resources.
|
||||
*/
|
||||
default void suspend() {
|
||||
default void dispose() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public final class RPCDeviceBusAdapter implements Steppable {
|
||||
private final SerialDevice serialDevice;
|
||||
private final Gson gson;
|
||||
|
||||
private final ArrayList<RPCDeviceWithIdentifier> devices = new ArrayList<>();
|
||||
private final ArrayList<RPCDeviceWithIdentifier> devicesWithId = new ArrayList<>();
|
||||
private final HashMap<UUID, RPCDeviceList> devicesById = new HashMap<>();
|
||||
private final Set<RPCDeviceList> unmountedDevices = new HashSet<>();
|
||||
private final Set<RPCDeviceList> mountedDevices = new HashSet<>();
|
||||
@@ -82,14 +82,18 @@ public final class RPCDeviceBusAdapter implements Steppable {
|
||||
for (final RPCDevice device : mountedDevices) {
|
||||
device.unmount();
|
||||
}
|
||||
unmountedDevices.addAll(mountedDevices);
|
||||
mountedDevices.clear();
|
||||
}
|
||||
|
||||
public void suspend() {
|
||||
for (final RPCDeviceWithIdentifier info : devices) {
|
||||
info.device.suspend();
|
||||
public void dispose() {
|
||||
for (final RPCDevice device : mountedDevices) {
|
||||
device.unmount();
|
||||
device.dispose();
|
||||
}
|
||||
for (final RPCDeviceList device : unmountedDevices) {
|
||||
device.dispose();
|
||||
}
|
||||
unmountedDevices.addAll(mountedDevices);
|
||||
mountedDevices.clear();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
@@ -115,10 +119,6 @@ public final class RPCDeviceBusAdapter implements Steppable {
|
||||
return;
|
||||
}
|
||||
|
||||
devices.clear();
|
||||
devicesById.clear();
|
||||
unmountedDevices.clear();
|
||||
|
||||
// How device grouping works:
|
||||
// Each device can have multiple UUIDs due to being attached to multiple bus elements.
|
||||
// There is no guarantee that for each device D1 present on bus elements E1 and E2,
|
||||
@@ -162,31 +162,38 @@ public final class RPCDeviceBusAdapter implements Steppable {
|
||||
.add(identifier);
|
||||
});
|
||||
|
||||
final Set<RPCDeviceList> newDevices = new HashSet<>();
|
||||
// Rebuild devices lists.
|
||||
devicesWithId.clear();
|
||||
devicesById.clear();
|
||||
|
||||
final Set<RPCDeviceList> devices = new HashSet<>();
|
||||
identifiersByDevice.forEach((device, identifiers) -> {
|
||||
final UUID identifier = selectIdentifierDeterministically(identifiers);
|
||||
devices.add(new RPCDeviceWithIdentifier(identifier, device));
|
||||
devicesWithId.add(new RPCDeviceWithIdentifier(identifier, device));
|
||||
devicesById.put(identifier, device);
|
||||
newDevices.add(device);
|
||||
devices.add(device);
|
||||
|
||||
// Add to set of unmounted devices if we don't already track it. It's a set, so
|
||||
// there won't be duplicates in the unmounted set due to this.
|
||||
if (!mountedDevices.contains(device)) {
|
||||
unmountedDevices.add(device);
|
||||
}
|
||||
});
|
||||
|
||||
// Add new devices to list of unmounted devices. List was cleared, so removed devices previously in
|
||||
// list of unmounted devices are already gone.
|
||||
for (final RPCDeviceList newDevice : newDevices) {
|
||||
if (!mountedDevices.contains(newDevice)) {
|
||||
unmountedDevices.add(newDevice);
|
||||
}
|
||||
}
|
||||
// Remove devices from mounted set, call appropriate callbacks.
|
||||
final HashSet<RPCDeviceList> removedMountedDevices = new HashSet<>(mountedDevices);
|
||||
removedMountedDevices.removeAll(devices);
|
||||
mountedDevices.removeAll(removedMountedDevices);
|
||||
removedMountedDevices.forEach(device -> {
|
||||
device.unmount();
|
||||
device.dispose();
|
||||
});
|
||||
|
||||
// Remove removed devices from list of mounted devices.
|
||||
final Iterator<RPCDeviceList> mountedDeviceIterator = mountedDevices.iterator();
|
||||
while (mountedDeviceIterator.hasNext()) {
|
||||
final RPCDeviceList device = mountedDeviceIterator.next();
|
||||
if (!newDevices.contains(device)) {
|
||||
device.unmount();
|
||||
mountedDeviceIterator.remove();
|
||||
}
|
||||
}
|
||||
// Remove devices from unmounted set, call appropriate callbacks.
|
||||
final HashSet<RPCDeviceList> removedUnmountedDevices = new HashSet<>(unmountedDevices);
|
||||
removedUnmountedDevices.removeAll(devices);
|
||||
unmountedDevices.removeAll(removedUnmountedDevices);
|
||||
removedUnmountedDevices.forEach(RPCDeviceList::dispose);
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
@@ -355,7 +362,7 @@ public final class RPCDeviceBusAdapter implements Steppable {
|
||||
}
|
||||
|
||||
private void writeDeviceList() {
|
||||
writeMessage(Message.MESSAGE_TYPE_LIST, devices);
|
||||
writeMessage(Message.MESSAGE_TYPE_LIST, devicesWithId);
|
||||
}
|
||||
|
||||
private void writeDeviceMethods(final UUID deviceId) {
|
||||
|
||||
@@ -42,7 +42,7 @@ public abstract class AbstractItemRPCDevice extends IdentityProxy<ItemStack> imp
|
||||
}
|
||||
|
||||
@Override
|
||||
public void suspend() {
|
||||
device.suspend();
|
||||
public void dispose() {
|
||||
device.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ public final class FileImportExportCardItemDevice extends AbstractItemRPCDevice
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void suspend() {
|
||||
public void unmount() {
|
||||
reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -42,9 +42,9 @@ public record RPCDeviceList(ArrayList<RPCDevice> devices) implements RPCDevice {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void suspend() {
|
||||
public void dispose() {
|
||||
for (final RPCDevice device : devices) {
|
||||
device.suspend();
|
||||
device.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ public abstract class AbstractVirtualMachine implements VirtualMachine {
|
||||
public void suspend() {
|
||||
joinWorkerThread();
|
||||
state.vmAdapter.suspend();
|
||||
state.rpcAdapter.suspend();
|
||||
state.rpcAdapter.unmount();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -203,7 +203,7 @@ public abstract class AbstractVirtualMachine implements VirtualMachine {
|
||||
state.board.setRunning(false);
|
||||
state.board.reset();
|
||||
state.rpcAdapter.reset();
|
||||
state.rpcAdapter.unmount();
|
||||
state.rpcAdapter.dispose();
|
||||
state.vmAdapter.unmount();
|
||||
|
||||
runner = null;
|
||||
|
||||
@@ -30,134 +30,94 @@ public final class RPCDeviceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyDevicesAreNotMounted() {
|
||||
final RPCDevice device1 = mock(RPCDevice.class);
|
||||
addDevice(device1);
|
||||
public void resumeDoesNotMountDirectly() {
|
||||
final RPCDevice device1 = addDevice();
|
||||
|
||||
adapter.resume(controller, true);
|
||||
adapter.mount();
|
||||
verify(device1, never()).mount();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyDevicesAreNotMounted() {
|
||||
final RPCDevice device = addEmptyDevice();
|
||||
adapter.resume(controller, true);
|
||||
|
||||
adapter.mount();
|
||||
verify(device, never()).mount();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addedDevicesHaveMountCalled() {
|
||||
final RPCDevice device1 = mock(RPCDevice.class);
|
||||
when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class)));
|
||||
addDevice(device1);
|
||||
|
||||
final RPCDevice device = addDevice();
|
||||
adapter.resume(controller, true);
|
||||
verify(device1, never()).mount();
|
||||
|
||||
adapter.mount();
|
||||
verify(device1).mount();
|
||||
verify(device).mount();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mountedDevicesAreUnmountedWhenRemoved() {
|
||||
final RPCDevice device1 = mock(RPCDevice.class);
|
||||
when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class)));
|
||||
addDevice(device1);
|
||||
|
||||
public void mountedDevicesAreUnmountedAndDisposedWhenRemoved() {
|
||||
final RPCDevice device = addDevice();
|
||||
adapter.resume(controller, true);
|
||||
verify(device1, never()).mount();
|
||||
|
||||
adapter.mount();
|
||||
verify(device1).mount();
|
||||
|
||||
removeDevice(device1);
|
||||
removeDevice(device);
|
||||
adapter.resume(controller, true);
|
||||
verify(device1).unmount();
|
||||
verify(device).unmount();
|
||||
verify(device).dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mountedDevicesAreUnmountedOnGlobalUnmount() {
|
||||
final RPCDevice device1 = mock(RPCDevice.class);
|
||||
when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class)));
|
||||
addDevice(device1);
|
||||
|
||||
public void unmountedDevicesAreDisposedWhenRemoved() {
|
||||
final RPCDevice device = addDevice();
|
||||
adapter.resume(controller, true);
|
||||
verify(device1, never()).mount();
|
||||
|
||||
removeDevice(device);
|
||||
adapter.resume(controller, true);
|
||||
verify(device, never()).unmount();
|
||||
verify(device).dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mountedDevicesAreUnmountedButNotDisposedOnGlobalUnmount() {
|
||||
final RPCDevice device = addDevice();
|
||||
adapter.resume(controller, true);
|
||||
adapter.mount();
|
||||
verify(device1).mount();
|
||||
|
||||
adapter.unmount();
|
||||
verify(device1).unmount();
|
||||
verify(device).unmount();
|
||||
verify(device, never()).dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unmountedDevicesAreNotUnmountedOnGlobalUnmount() {
|
||||
final RPCDevice device1 = mock(RPCDevice.class);
|
||||
when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class)));
|
||||
addDevice(device1);
|
||||
|
||||
public void unmountedDevicesAreNotUnmountedAndNotDisposedOnGlobalUnmount() {
|
||||
final RPCDevice device = addDevice();
|
||||
adapter.resume(controller, true);
|
||||
verify(device1, never()).mount();
|
||||
|
||||
adapter.unmount();
|
||||
verify(device1, never()).unmount();
|
||||
verify(device, never()).unmount();
|
||||
verify(device, never()).dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deviceListForwardsMount() {
|
||||
final RPCDevice device1 = mock(RPCDevice.class);
|
||||
final RPCDevice device2 = mock(RPCDevice.class);
|
||||
final RPCDevice listDevice = new RPCDeviceList(new ArrayList<>(Arrays.asList(device1, device2)));
|
||||
when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class)));
|
||||
when(device2.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class)));
|
||||
addDevice(listDevice);
|
||||
|
||||
public void mountedDevicesAreUnmountedAndDisposedOnGlobalDispose() {
|
||||
final RPCDevice device = addDevice();
|
||||
adapter.resume(controller, true);
|
||||
verify(device1, never()).mount();
|
||||
verify(device2, never()).mount();
|
||||
|
||||
adapter.mount();
|
||||
verify(device1).mount();
|
||||
verify(device2).mount();
|
||||
|
||||
adapter.dispose();
|
||||
verify(device).unmount();
|
||||
verify(device).dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deviceListForwardsUnmount() {
|
||||
final RPCDevice device1 = mock(RPCDevice.class);
|
||||
final RPCDevice device2 = mock(RPCDevice.class);
|
||||
final RPCDevice listDevice = new RPCDeviceList(new ArrayList<>(Arrays.asList(device1, device2)));
|
||||
when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class)));
|
||||
when(device2.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class)));
|
||||
addDevice(listDevice);
|
||||
|
||||
public void unmountedDevicesAreNotUnmountedButDisposedOnGlobalDispose() {
|
||||
final RPCDevice device = addDevice();
|
||||
adapter.resume(controller, true);
|
||||
verify(device1, never()).mount();
|
||||
verify(device2, never()).mount();
|
||||
|
||||
adapter.mount();
|
||||
verify(device1).mount();
|
||||
verify(device2).mount();
|
||||
|
||||
adapter.unmount();
|
||||
verify(device1).unmount();
|
||||
verify(device2).unmount();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deviceListForwardsSuspend() {
|
||||
final RPCDevice device1 = mock(RPCDevice.class);
|
||||
final RPCDevice device2 = mock(RPCDevice.class);
|
||||
final RPCDevice listDevice = new RPCDeviceList(new ArrayList<>(Arrays.asList(device1, device2)));
|
||||
when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class)));
|
||||
when(device2.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class)));
|
||||
addDevice(listDevice);
|
||||
|
||||
adapter.resume(controller, true);
|
||||
verify(device1, never()).mount();
|
||||
verify(device2, never()).mount();
|
||||
|
||||
adapter.mount();
|
||||
verify(device1).mount();
|
||||
verify(device2).mount();
|
||||
|
||||
adapter.suspend();
|
||||
verify(device1).suspend();
|
||||
verify(device2).suspend();
|
||||
adapter.dispose();
|
||||
verify(device, never()).unmount();
|
||||
verify(device).dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -187,6 +147,19 @@ public final class RPCDeviceTests {
|
||||
verify(device2, atMostOnce()).mount();
|
||||
}
|
||||
|
||||
private RPCDevice addEmptyDevice() {
|
||||
final RPCDevice device = mock(RPCDevice.class);
|
||||
addDevice(device);
|
||||
return device;
|
||||
}
|
||||
|
||||
private RPCDevice addDevice() {
|
||||
final RPCDevice device = mock(RPCDevice.class);
|
||||
when(device.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class)));
|
||||
addDevice(device);
|
||||
return device;
|
||||
}
|
||||
|
||||
private void addDevice(final Device device, UUID... identifiers) {
|
||||
if (identifiers.length == 0) {
|
||||
identifiers = new UUID[]{UUID.randomUUID()};
|
||||
|
||||
Reference in New Issue
Block a user