Add mount and unmount lifecycle callbacks to RPCDevices.
This commit is contained in:
@@ -44,6 +44,24 @@ public interface RPCDevice extends Device {
|
||||
*/
|
||||
List<RPCMethod> getMethods();
|
||||
|
||||
/**
|
||||
* Called to initialize this device.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,6 +40,8 @@ public final class RPCDeviceBusAdapter implements Steppable {
|
||||
|
||||
private final ArrayList<RPCDeviceWithIdentifier> devices = new ArrayList<>();
|
||||
private final HashMap<UUID, RPCDeviceList> devicesById = new HashMap<>();
|
||||
private final Set<RPCDevice> unmountedDevices = new HashSet<>();
|
||||
private final Set<RPCDevice> 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<RPCDevice> 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<RPCDevice> mountedDeviceIterator = mountedDevices.iterator();
|
||||
while (mountedDeviceIterator.hasNext()) {
|
||||
final RPCDevice device = mountedDeviceIterator.next();
|
||||
if (!newDevices.contains(device)) {
|
||||
device.unmount();
|
||||
mountedDeviceIterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
203
src/test/java/li/cil/oc2/common/bus/RPCDeviceTests.java
Normal file
203
src/test/java/li/cil/oc2/common/bus/RPCDeviceTests.java
Normal file
@@ -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<Device> busDevices;
|
||||
private Map<Device, Set<UUID>> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user