1 package org.robolectric.shadows;
2 
3 import android.os.Build;
4 import android.os.SharedMemory;
5 import android.system.ErrnoException;
6 import android.system.OsConstants;
7 import java.io.File;
8 import java.io.FileDescriptor;
9 import java.io.IOException;
10 import java.io.RandomAccessFile;
11 import java.nio.ByteBuffer;
12 import java.nio.MappedByteBuffer;
13 import java.nio.channels.FileChannel.MapMode;
14 import java.nio.file.Files;
15 import java.util.Map;
16 import java.util.concurrent.ConcurrentHashMap;
17 import java.util.concurrent.atomic.AtomicReference;
18 import org.robolectric.RuntimeEnvironment;
19 import org.robolectric.annotation.Implementation;
20 import org.robolectric.annotation.Implements;
21 import org.robolectric.annotation.RealObject;
22 import org.robolectric.annotation.Resetter;
23 import org.robolectric.util.ReflectionHelpers;
24 import org.robolectric.util.TempDirectory;
25 
26 /**
27  * A {@link SharedMemory} fake that uses a private temporary disk file for storage and Java's {@link
28  * MappedByteBuffer} for the memory mappings.
29  */
30 @Implements(
31     value = SharedMemory.class,
32     minSdk = Build.VERSION_CODES.O_MR1,
33     /* not quite true, but this prevents a useless `shadowOf()` accessor showing
34      * up, which would break people compiling against API 26 and earlier.
35      */
36     isInAndroidSdk = false)
37 public class ShadowSharedMemory {
38   private static final Map<Integer, File> filesByFd = new ConcurrentHashMap<>();
39 
40   private static final AtomicReference<ErrnoException> fakeCreateException =
41       new AtomicReference<>();
42 
43   @RealObject private SharedMemory realObject;
44 
45   @Resetter
reset()46   public static void reset() {
47     filesByFd.clear();
48   }
49 
50   /**
51    * Only works on {@link SharedMemory} instances from {@link SharedMemory#create}.
52    *
53    * <p>"prot" is ignored -- all mappings are read/write.
54    */
55   @Implementation
map(int prot, int offset, int length)56   protected ByteBuffer map(int prot, int offset, int length) throws ErrnoException {
57     ReflectionHelpers.callInstanceMethod(realObject, "checkOpen");
58     FileDescriptor fileDescriptor = getRealFileDescriptor();
59     int fd = ReflectionHelpers.getField(fileDescriptor, "fd");
60     File file = filesByFd.get(fd);
61     if (file == null) {
62       throw new IllegalStateException("Cannot find the backing file from fd");
63     }
64 
65     try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw")) {
66       // It would be easy to support a MapMode.READ_ONLY type mapping as well except none of the
67       // OsConstants fields are even initialized by robolectric and so "prot" is always zero!
68       return randomAccessFile.getChannel().map(MapMode.READ_WRITE, offset, length);
69     } catch (IOException e) {
70       throw new ErrnoException(e.getMessage(), OsConstants.EIO, e);
71     }
72   }
73 
74   @Implementation
unmap(ByteBuffer mappedBuf)75   protected static void unmap(ByteBuffer mappedBuf) throws ErrnoException {
76     if (mappedBuf instanceof MappedByteBuffer) {
77       // There's no API to clean up a MappedByteBuffer other than GC so we've got nothing to do!
78     } else {
79       throw new IllegalArgumentException(
80           "ByteBuffer wasn't created by #map(int, int, int); can't unmap");
81     }
82   }
83 
84   @Implementation
nCreate(String name, int size)85   protected static FileDescriptor nCreate(String name, int size) throws ErrnoException {
86     maybeThrow(fakeCreateException);
87 
88     TempDirectory tempDirectory = RuntimeEnvironment.getTempDirectory();
89 
90     try {
91       // Give each instance its own private file.
92       File sharedMemoryFile =
93           Files.createTempFile(
94                   tempDirectory.createIfNotExists("SharedMemory"), "shmem-" + name, ".tmp")
95               .toFile();
96       RandomAccessFile randomAccessFile = new RandomAccessFile(sharedMemoryFile, "rw");
97       randomAccessFile.setLength(0);
98       randomAccessFile.setLength(size);
99 
100       FileDescriptor fileDescriptor = randomAccessFile.getFD();
101       int fd = ReflectionHelpers.getField(fileDescriptor, "fd");
102       filesByFd.put(fd, sharedMemoryFile);
103       return fileDescriptor;
104     } catch (IOException e) {
105       throw new RuntimeException("Unable to create file descriptior", e);
106     }
107   }
108 
109   @Implementation
nGetSize(FileDescriptor fd)110   protected static int nGetSize(FileDescriptor fd) {
111     int internalFd = ReflectionHelpers.getField(fd, "fd");
112     return (int) filesByFd.get(internalFd).length();
113   }
114 
getRealFileDescriptor()115   private FileDescriptor getRealFileDescriptor() {
116     return ReflectionHelpers.getField(realObject, "mFileDescriptor");
117   }
118 
119   /**
120    * Causes subsequent calls to {@link SharedMemory#create)} to throw the specified exception, if
121    * non-null. Pass null to restore create to normal operation.
122    */
setCreateShouldThrow(ErrnoException e)123   public static void setCreateShouldThrow(ErrnoException e) {
124     fakeCreateException.set(e);
125   }
126 
maybeThrow(AtomicReference<ErrnoException> exceptionRef)127   private static void maybeThrow(AtomicReference<ErrnoException> exceptionRef)
128       throws ErrnoException {
129     ErrnoException exception = exceptionRef.get();
130     if (exception != null) {
131       throw exception;
132     }
133   }
134 }
135