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:
Florian Nücke
2022-01-27 20:10:41 +01:00
parent bd1b46882e
commit 82a6702b33
10 changed files with 150 additions and 173 deletions

View File

@@ -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) {

View File

@@ -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() {
}
}

View File

@@ -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();
}
}

View File

@@ -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() {
}
}

View File

@@ -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) {

View File

@@ -42,7 +42,7 @@ public abstract class AbstractItemRPCDevice extends IdentityProxy<ItemStack> imp
}
@Override
public void suspend() {
device.suspend();
public void dispose() {
device.dispose();
}
}

View File

@@ -134,7 +134,7 @@ public final class FileImportExportCardItemDevice extends AbstractItemRPCDevice
///////////////////////////////////////////////////////////////////
@Override
public void suspend() {
public void unmount() {
reset();
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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()};