diff --git a/src/main/java/li/cil/oc2/api/bus/device/rpc/RPCDevice.java b/src/main/java/li/cil/oc2/api/bus/device/rpc/RPCDevice.java index ed5123dc..c9574735 100644 --- a/src/main/java/li/cil/oc2/api/bus/device/rpc/RPCDevice.java +++ b/src/main/java/li/cil/oc2/api/bus/device/rpc/RPCDevice.java @@ -44,6 +44,24 @@ public interface RPCDevice extends Device { */ List getMethods(); + /** + * Called to initialize this device. + *

+ * This is called when the connected virtual machine starts, or when the device is added to an already running + * virtual machine. + */ + default void mount() { + } + + /** + * Called to dispose this device. + *

+ * Called when the connected virtual machine stops, or when the device is removed from a currently running + * virtual machine. + */ + default void unmount() { + } + /** * Called when the device is suspended. *

diff --git a/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java b/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java index f53f812e..79d50090 100644 --- a/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java +++ b/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java @@ -199,10 +199,7 @@ public final class ComputerBlockEntity extends ModBlockEntity implements Termina public void setRemoved() { super.setRemoved(); - // super.remove() calls onUnload. This in turn only suspends, but we want to do - // a full clean-up when we get destroyed, so stuff inside us can delete out-of-nbt - // persisted runtime-only data such as ram. - virtualMachine.state.vmAdapter.unmount(); + virtualMachine.stop(); } @Override diff --git a/src/main/java/li/cil/oc2/common/bus/RPCDeviceBusAdapter.java b/src/main/java/li/cil/oc2/common/bus/RPCDeviceBusAdapter.java index 8b33102c..3775b950 100644 --- a/src/main/java/li/cil/oc2/common/bus/RPCDeviceBusAdapter.java +++ b/src/main/java/li/cil/oc2/common/bus/RPCDeviceBusAdapter.java @@ -40,6 +40,8 @@ public final class RPCDeviceBusAdapter implements Steppable { private final ArrayList devices = new ArrayList<>(); private final HashMap devicesById = new HashMap<>(); + private final Set unmountedDevices = new HashSet<>(); + private final Set mountedDevices = new HashSet<>(); private final Lock pauseLock = new ReentrantLock(); private boolean isPaused; @@ -69,6 +71,22 @@ public final class RPCDeviceBusAdapter implements Steppable { /////////////////////////////////////////////////////////////////// + public void mount() { + for (final RPCDevice device : unmountedDevices) { + device.mount(); + } + mountedDevices.addAll(unmountedDevices); + unmountedDevices.clear(); + } + + public void unmount() { + for (final RPCDevice device : mountedDevices) { + device.unmount(); + } + unmountedDevices.addAll(mountedDevices); + mountedDevices.clear(); + } + public void suspend() { for (final RPCDeviceWithIdentifier info : devices) { info.device.suspend(); @@ -100,6 +118,7 @@ public final class RPCDeviceBusAdapter implements Steppable { devices.clear(); devicesById.clear(); + unmountedDevices.clear(); // How device grouping works: // Each device can have multiple UUIDs due to being attached to multiple bus elements. @@ -144,11 +163,31 @@ public final class RPCDeviceBusAdapter implements Steppable { .add(identifier); }); + final Set newDevices = new HashSet<>(); identifiersByDevice.forEach((device, identifiers) -> { final UUID identifier = selectIdentifierDeterministically(identifiers); devices.add(new RPCDeviceWithIdentifier(identifier, device)); devicesById.put(identifier, device); + newDevices.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 RPCDevice newDevice : newDevices) { + if (!mountedDevices.contains(newDevice)) { + unmountedDevices.add(newDevice); + } + } + + // Remove removed devices from list of mounted devices. + final Iterator mountedDeviceIterator = mountedDevices.iterator(); + while (mountedDeviceIterator.hasNext()) { + final RPCDevice device = mountedDeviceIterator.next(); + if (!newDevices.contains(device)) { + device.unmount(); + mountedDeviceIterator.remove(); + } + } } public void tick() { diff --git a/src/main/java/li/cil/oc2/common/bus/device/rpc/RPCDeviceList.java b/src/main/java/li/cil/oc2/common/bus/device/rpc/RPCDeviceList.java index 91aaaab8..21d7a14e 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/rpc/RPCDeviceList.java +++ b/src/main/java/li/cil/oc2/common/bus/device/rpc/RPCDeviceList.java @@ -35,6 +35,20 @@ public final class RPCDeviceList implements RPCDevice { .collect(Collectors.toList()); } + @Override + public void mount() { + for (final RPCDevice device : devices) { + device.mount(); + } + } + + @Override + public void unmount() { + for (final RPCDevice device : devices) { + device.unmount(); + } + } + @Override public void suspend() { for (final RPCDevice device : devices) { diff --git a/src/main/java/li/cil/oc2/common/entity/Robot.java b/src/main/java/li/cil/oc2/common/entity/Robot.java index 81f450e9..2c2fb372 100644 --- a/src/main/java/li/cil/oc2/common/entity/Robot.java +++ b/src/main/java/li/cil/oc2/common/entity/Robot.java @@ -348,7 +348,7 @@ public final class Robot extends Entity implements li.cil.oc2.api.capabilities.R virtualMachine.suspend(); // Full unload to release out-of-nbt persisted runtime-only data such as ram. - virtualMachine.state.vmAdapter.unmount(); + virtualMachine.stop(); } @Override diff --git a/src/main/java/li/cil/oc2/common/vm/AbstractVirtualMachine.java b/src/main/java/li/cil/oc2/common/vm/AbstractVirtualMachine.java index 5cde6bb8..e8e41d27 100644 --- a/src/main/java/li/cil/oc2/common/vm/AbstractVirtualMachine.java +++ b/src/main/java/li/cil/oc2/common/vm/AbstractVirtualMachine.java @@ -198,6 +198,7 @@ public abstract class AbstractVirtualMachine implements VirtualMachine { state.board.reset(); state.rpcAdapter.reset(); + state.rpcAdapter.unmount(); state.vmAdapter.unmount(); runner = null; @@ -337,6 +338,8 @@ public abstract class AbstractVirtualMachine implements VirtualMachine { runner = createRunner(); } + state.rpcAdapter.mount(); + setRunState(VMRunState.RUNNING); // Only start running next tick. This gives loaded devices one tick to do async diff --git a/src/test/java/li/cil/oc2/common/bus/RPCDeviceTests.java b/src/test/java/li/cil/oc2/common/bus/RPCDeviceTests.java new file mode 100644 index 00000000..9f23dba9 --- /dev/null +++ b/src/test/java/li/cil/oc2/common/bus/RPCDeviceTests.java @@ -0,0 +1,203 @@ +package li.cil.oc2.common.bus; + +import li.cil.oc2.api.bus.DeviceBusController; +import li.cil.oc2.api.bus.device.Device; +import li.cil.oc2.api.bus.device.rpc.RPCDevice; +import li.cil.oc2.api.bus.device.rpc.RPCMethod; +import li.cil.oc2.common.bus.device.rpc.RPCDeviceList; +import li.cil.sedna.api.device.serial.SerialDevice; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.mockito.Mockito.*; + +public final class RPCDeviceTests { + private RPCDeviceBusAdapter adapter; + private Set busDevices; + private Map> deviceIdentifiers; + private DeviceBusController controller; + + @BeforeEach + public void setupEach() { + adapter = new RPCDeviceBusAdapter(mock(SerialDevice.class)); + busDevices = new HashSet<>(); + deviceIdentifiers = new HashMap<>(); + controller = mock(DeviceBusController.class); + when(controller.getDevices()).thenReturn(busDevices); + when(controller.getDeviceIdentifiers(any())).then(invocation -> deviceIdentifiers.get((Device) invocation.getArgument(0))); + } + + @Test + public void emptyDevicesAreNotMounted() { + final RPCDevice device1 = mock(RPCDevice.class); + addDevice(device1); + + adapter.resume(controller, true); + adapter.mount(); + verify(device1, never()).mount(); + } + + @Test + public void addedDevicesHaveMountCalled() { + final RPCDevice device1 = mock(RPCDevice.class); + when(device1.getMethods()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); + addDevice(device1); + + adapter.resume(controller, true); + verify(device1, never()).mount(); + + adapter.mount(); + verify(device1).mount(); + } + + @Test + public void mountedDevicesAreUnmountedWhenRemoved() { + final RPCDevice device1 = mock(RPCDevice.class); + when(device1.getMethods()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); + addDevice(device1); + + adapter.resume(controller, true); + verify(device1, never()).mount(); + + adapter.mount(); + verify(device1).mount(); + + removeDevice(device1); + adapter.resume(controller, true); + verify(device1).unmount(); + } + + @Test + public void mountedDevicesAreUnmountedOnGlobalUnmount() { + final RPCDevice device1 = mock(RPCDevice.class); + when(device1.getMethods()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); + addDevice(device1); + + adapter.resume(controller, true); + verify(device1, never()).mount(); + + adapter.mount(); + verify(device1).mount(); + + adapter.unmount(); + verify(device1).unmount(); + } + + @Test + public void unmountedDevicesAreNotUnmountedOnGlobalUnmount() { + final RPCDevice device1 = mock(RPCDevice.class); + when(device1.getMethods()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); + addDevice(device1); + + adapter.resume(controller, true); + verify(device1, never()).mount(); + + adapter.unmount(); + verify(device1, never()).unmount(); + } + + @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.getMethods()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); + when(device2.getMethods()).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(); + } + + @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.getMethods()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); + when(device2.getMethods()).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.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.getMethods()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); + when(device2.getMethods()).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(); + } + + @Test + public void deviceListIsStable() { + 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.getMethods()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); + when(device2.getMethods()).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.resume(controller, true); + + verify(device1, never()).unmount(); + verify(device2, never()).unmount(); + + adapter.mount(); + verify(device1, atMostOnce()).mount(); + verify(device2, atMostOnce()).mount(); + } + + private void addDevice(final Device device, UUID... identifiers) { + if (identifiers.length == 0) { + identifiers = new UUID[]{UUID.randomUUID()}; + } + + busDevices.add(device); + deviceIdentifiers.put(device, new HashSet<>(Arrays.asList(identifiers))); + } + + private void removeDevice(final Device device) { + busDevices.remove(device); + deviceIdentifiers.remove(device); + } +}