Skip to content

Commit 2eb4df3

Browse files
committed
Feature: Allow panic to be detected in Java Level. Feature: Export logs.
1 parent 99308ca commit 2eb4df3

File tree

3 files changed

+361
-48
lines changed

3 files changed

+361
-48
lines changed

.github/workflows/build.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ jobs:
174174
cargo +nightly ndk build --release --lib --target "${{ matrix.target }}" \
175175
-Z build-std=core,std,alloc,proc_macro,panic_abort \
176176
-Z build-std-features=default,optimize_for_size
177+
env:
178+
RUSTFLAGS: -Zunstable-options -Cpanic=unwind
177179
- name: Upload Artifact
178180
run: |
179181
if [ "${{ matrix.os }}" = "android" ]; then

ffi/TerracottaAndroidAPI.java

Lines changed: 239 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,20 @@
77

88
import androidx.annotation.Nullable;
99

10+
import java.io.BufferedReader;
11+
import java.io.File;
1012
import java.io.IOException;
11-
import java.io.UncheckedIOException;
13+
import java.io.InputStream;
14+
import java.io.InputStreamReader;
15+
import java.io.RandomAccessFile;
16+
import java.io.Reader;
1217
import java.net.InetAddress;
1318
import java.net.UnknownHostException;
14-
import java.nio.file.Files;
15-
import java.nio.file.Path;
19+
import java.nio.charset.StandardCharsets;
1620
import java.util.Arrays;
1721
import java.util.Objects;
22+
import java.util.concurrent.atomic.AtomicBoolean;
1823
import java.util.concurrent.atomic.AtomicLong;
19-
import java.util.concurrent.atomic.AtomicReference;
2024

2125
/**
2226
* <p>An API to handle Terracotta Android.</p>
@@ -43,6 +47,10 @@
4347
* or Terracotta would stuck and EasyTier cannot submit a new VpnService Request.</p>
4448
*
4549
* <p>The VpnServiceRequest must be fulfilled in 30 seconds, or it will be considered as timeout.</p>
50+
*
51+
* <h1>Panic</h1>
52+
*
53+
* <p>A RuntimeException will be thrown if an error occured in native level.</p>
4654
*/
4755
public final class TerracottaAndroidAPI {
4856
/**
@@ -68,9 +76,8 @@ public interface VpnServiceRequest {
6876
* @param builder A pre-configured VpnService builder.
6977
* @return The established Vpn Connection. Developers must close this file descriptor after EasyTier exits.
7078
* @throws RuntimeException if {@link VpnService.Builder#establish()} returns null.
71-
*
7279
* @implNote Developers is able to configure the builder before passing it into Terracotta
73-
* to fully-custom the connection.
80+
* to fully-custom the connection.
7481
*/
7582
ParcelFileDescriptor startVpnService(VpnService.Builder builder);
7683

@@ -80,16 +87,66 @@ public interface VpnServiceRequest {
8087
void reject();
8188
}
8289

90+
/**
91+
* <p>Metadata of Terracotta Android</p>
92+
*/
93+
public static final class Metadata {
94+
private final String terracottaVersion;
95+
96+
private final long terracottaCompileTime;
97+
98+
private final String easyTierVersion;
99+
100+
private Metadata(String terracottaVersion, long terracottaCompileTime, String easyTierVersion) {
101+
this.terracottaVersion = terracottaVersion;
102+
this.terracottaCompileTime = terracottaCompileTime;
103+
this.easyTierVersion = easyTierVersion;
104+
}
105+
106+
/**
107+
* @return Terracotta Android version.
108+
*/
109+
public String getTerracottaVersion() {
110+
return terracottaVersion;
111+
}
112+
113+
/**
114+
* @return Terracotta Android compile timestamp. The format is identical with {@link System#currentTimeMillis()}.
115+
*/
116+
public long getTerracottaCompileTime() {
117+
return terracottaCompileTime;
118+
}
119+
120+
/**
121+
* @return EasyTier version.
122+
*/
123+
public String getEasyTierVersion() {
124+
return easyTierVersion;
125+
}
126+
}
127+
83128
static {
84129
System.loadLibrary("terracotta");
85130
}
86131

87132
private static volatile VpnServiceRequest pendingRequest = null;
88133

89-
private static final AtomicReference<VpnServiceCallback> VPN_SERVICE_CALLBACK = new AtomicReference<>(null);
134+
private static final class RuntimeContext {
135+
private final VpnServiceCallback vpnServiceCallback;
136+
137+
private final RandomAccessFile logging;
138+
139+
public RuntimeContext(VpnServiceCallback vpnServiceCallback, RandomAccessFile logging) {
140+
this.vpnServiceCallback = vpnServiceCallback;
141+
this.logging = logging;
142+
}
143+
}
144+
145+
private static volatile RuntimeContext runtimeContext = null;
90146

91147
/**
92148
* <p>Get current pending VpnService Request.</p>
149+
*
93150
* @return The pending VpnService Request.
94151
* @throws IllegalStateException if no pending VpnService Request exists.
95152
*/
@@ -107,18 +164,43 @@ public static VpnServiceRequest getPendingVpnServiceRequest() {
107164
* @param context An Android context object.
108165
* @param callback A callback to handle VpnService for EasyTier. See {@link VpnServiceCallback} for more information.
109166
*/
110-
public static void initialize(Context context, VpnServiceCallback callback) {
111-
if (VPN_SERVICE_CALLBACK.compareAndSet(null, callback)) {
112-
Path base = context.getFilesDir().toPath().resolve("net.burningtnt.terracotta/rs");
113-
try {
114-
Files.createDirectories(base);
115-
} catch (IOException e) {
116-
throw new UncheckedIOException(e);
117-
}
118-
start0(base.toString());
119-
} else {
167+
public static synchronized Metadata initialize(Context context, VpnServiceCallback callback) {
168+
Objects.requireNonNull(context, "context");
169+
Objects.requireNonNull(callback, "callback");
170+
171+
if (runtimeContext != null) {
120172
throw new IllegalStateException("Terracotta Android has already started.");
121173
}
174+
175+
File root = new File(context.getFilesDir(), "net.burningtnt.terracotta");
176+
177+
File base = new File(root, "rs");
178+
base.mkdirs();
179+
if (!base.isDirectory()) {
180+
throw new RuntimeException("Cannot create net.burningtnt.terracotta/rs directory.");
181+
}
182+
183+
RandomAccessFile logging;
184+
int fd;
185+
try {
186+
logging = new RandomAccessFile(new File(root, "application.log"), "rw");
187+
fd = ParcelFileDescriptor.dup(logging.getFD()).detachFd();
188+
} catch (IOException e) {
189+
throw new RuntimeException(e);
190+
}
191+
192+
int code = start0(base.getAbsolutePath(), fd);
193+
if (code != 0) {
194+
throw new RuntimeException("Cannot start Terracotta Android: " + code);
195+
}
196+
197+
runtimeContext = new RuntimeContext(callback, logging);
198+
199+
String[] parts = getMetadata0().split("\0", 4);
200+
if (parts.length != 3) {
201+
throw new AssertionError("Should NOT be here.");
202+
}
203+
return new Metadata(parts[0], Long.parseLong(parts[1]), parts[2]);
122204
}
123205

124206
/**
@@ -177,10 +259,130 @@ public static boolean setGuesting(String room, @Nullable String player) {
177259
return setGuesting0(room, player);
178260
}
179261

262+
/**
263+
* Room types supported by Terracotta Android
264+
*/
265+
public enum RoomType {
266+
TERRACOTTA_LEGACY, PCL2CE, SCAFFOLDING
267+
}
268+
269+
/**
270+
* <p>Parse the given room code.</p>
271+
*
272+
* @param room A room code.
273+
* @return The type of the room, or null if invalid.
274+
* @throws NullPointerException if room is null
275+
* @implNote Though this method is fast enough to be invoked in UI thread,
276+
* dispatch all invocation to a separated worker is better in order to
277+
* prevent potential UI freezing issues.
278+
*/
279+
public static RoomType parseRoomCode(String room) {
280+
Objects.requireNonNull(room, "room");
281+
282+
assertStarted();
283+
switch (verifyRoomCode0(room)) {
284+
case -1:
285+
return null;
286+
case 1:
287+
return RoomType.TERRACOTTA_LEGACY;
288+
case 2:
289+
return RoomType.PCL2CE;
290+
case 3:
291+
return RoomType.SCAFFOLDING;
292+
default:
293+
throw new AssertionError("Should NOT be here.");
294+
}
295+
}
296+
297+
/**
298+
* <p>Collect logs of Terracotta Android.</p>
299+
*
300+
* <p>Developers must immediately copy all data out of the returned reader and close it.
301+
* Otherwise all methods, except 'parseRoomCode', will be blocked.</p>
302+
*
303+
* @return A reader containing logs.
304+
* @throws RuntimeException if logging export is unsupported.
305+
*/
306+
public static Reader collectLogs() throws IOException {
307+
assertStarted();
308+
309+
long ptr = prepareExportLogs0();
310+
if (ptr == 0) {
311+
throw new RuntimeException("Cannot export logs from Terracotta Android.");
312+
}
313+
314+
RandomAccessFile file = runtimeContext.logging;
315+
file.seek(0);
316+
317+
return new BufferedReader(new InputStreamReader(new InputStream() {
318+
private final AtomicBoolean closed = new AtomicBoolean(false);
319+
320+
@Override
321+
public int read() throws IOException {
322+
assertOpen();
323+
return file.read();
324+
}
325+
326+
@Override
327+
public int read(byte[] b) throws IOException {
328+
assertOpen();
329+
return file.read(b);
330+
}
331+
332+
@Override
333+
public int read(byte[] b, int off, int len) throws IOException {
334+
assertOpen();
335+
return file.read(b, off, len);
336+
}
337+
338+
@Override
339+
public int available() throws IOException {
340+
assertOpen();
341+
return Math.toIntExact(file.length() - file.getFilePointer());
342+
}
343+
344+
@Override
345+
public void close() throws IOException {
346+
super.close();
347+
cleanup();
348+
}
349+
350+
@Override
351+
protected void finalize() throws Throwable {
352+
super.finalize();
353+
cleanup();
354+
}
355+
356+
private void assertOpen() throws IOException {
357+
if (closed.get()) {
358+
throw new IOException("Stream has already been closed");
359+
}
360+
}
361+
362+
private void cleanup() {
363+
if (closed.compareAndSet(false, true)) {
364+
finishExportLogs0(ptr);
365+
}
366+
}
367+
}, StandardCharsets.UTF_8));
368+
}
369+
370+
@Deprecated
371+
public static void panic() {
372+
panic0();
373+
374+
throw new AssertionError("Should NOT be here: A RuntimeException should be thrown in panic0");
375+
}
376+
180377
private static final long FD_PENDING = ((long) Integer.MAX_VALUE) + 1;
181378
private static final long FD_REJECT = FD_PENDING + 1;
182-
379+
;
380+
@SuppressWarnings("unused") // Native callback
183381
private static int onVpnServiceStateChanged(byte ip1, byte ip2, byte ip3, byte ip4, short network_length, String cidr) throws UnknownHostException {
382+
if (pendingRequest != null) {
383+
throw new AssertionError("Should NOT be here.");
384+
}
385+
184386
AtomicLong fd = new AtomicLong(FD_PENDING);
185387
InetAddress address = InetAddress.getByAddress(new byte[]{ip1, ip2, ip3, ip4});
186388

@@ -206,7 +408,7 @@ public ParcelFileDescriptor startVpnService(VpnService.Builder builder) {
206408
throw new RuntimeException("Cannot establish a VPN connection.");
207409
}
208410

209-
fd.set(connection.detachFd());
411+
fd.set(connection.getFd());
210412
return connection;
211413
}
212414

@@ -216,7 +418,7 @@ public void reject() {
216418
}
217419
};
218420

219-
VPN_SERVICE_CALLBACK.get().onStartVpnService();
421+
TerracottaAndroidAPI.runtimeContext.vpnServiceCallback.onStartVpnService();
220422

221423
long timestamp = System.currentTimeMillis();
222424
while (true) {
@@ -232,18 +434,22 @@ public void reject() {
232434
throw new IllegalStateException();
233435
} else {
234436
pendingRequest = null;
235-
return Math.toIntExact(value);
437+
438+
if ((int) value != value) {
439+
throw new AssertionError("Should NOT be here.");
440+
}
441+
return (int) value;
236442
}
237443
}
238444
}
239445

240446
private static void assertStarted() {
241-
if (VPN_SERVICE_CALLBACK.get() == null) {
447+
if (runtimeContext == null) {
242448
throw new IllegalStateException("Terracotta Android hasn't started yet.");
243449
}
244450
}
245451

246-
private static native void start0(String baseDir);
452+
private static native int start0(String baseDir, int loggingFD);
247453

248454
private static native String getState0();
249455

@@ -252,4 +458,14 @@ private static void assertStarted() {
252458
private static native void setScanning0(String room, String player);
253459

254460
private static native boolean setGuesting0(String room, String player);
461+
462+
private static native int verifyRoomCode0(String room);
463+
464+
private static native String getMetadata0();
465+
466+
private static native long prepareExportLogs0();
467+
468+
private static native void finishExportLogs0(long pointer);
469+
470+
private static native void panic0();
255471
}

0 commit comments

Comments
 (0)