Add mount and unmount lifecycle callbacks to RPCDevices.

This commit is contained in:
Florian Nücke
2022-01-15 14:29:48 +01:00
parent 6a2f55e731
commit 1085a6b7a4
7 changed files with 279 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);
}
}