delete CloudKit and added BalanceFlutter
Android CI / Build and Test (push) Failing after 21m18s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
Android CI / Build and Test (push) Failing after 21m18s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
This commit is contained in:
1 parent
a715eca947
commit
2c278044bb
214 files changed
+13411
-2290
No files matched your search
@@ -0,0 +1,20 @@
|
||||
group 'com.balance.runtime'
|
||||
version '1.0'
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
namespace 'com.balance.runtime'
|
||||
compileSdk 36
|
||||
defaultConfig {
|
||||
minSdk 23
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package com.balance.runtime;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import java.io.File;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
/** Serializes complete ledger/session operations across Flutter engines/processes. */
|
||||
public final class BalanceRuntimePlugin implements FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private final Handler main = new Handler(Looper.getMainLooper());
|
||||
private final Object state = new Object();
|
||||
private MethodChannel channel;
|
||||
private ExecutorService executor;
|
||||
private File lockFile;
|
||||
private boolean detached = true;
|
||||
private boolean acquiring;
|
||||
private LedgerFileLock held;
|
||||
|
||||
@Override
|
||||
public void onAttachedToEngine(FlutterPluginBinding binding) {
|
||||
synchronized (state) {
|
||||
detached = false;
|
||||
acquiring = false;
|
||||
lockFile = new File(binding.getApplicationContext().getFilesDir(), "balance-runtime.lock");
|
||||
executor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
channel = new MethodChannel(binding.getBinaryMessenger(), "balance/runtime");
|
||||
channel.setMethodCallHandler(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
|
||||
if ("acquireLock".equals(call.method)) {
|
||||
synchronized (state) {
|
||||
if (detached || acquiring || held != null) {
|
||||
result.error("lock_busy", "This engine already owns or is waiting for the ledger lock", null);
|
||||
return;
|
||||
}
|
||||
acquiring = true;
|
||||
executor.execute(() -> acquire(result));
|
||||
}
|
||||
} else if ("releaseLock".equals(call.method)) {
|
||||
LedgerFileLock release;
|
||||
synchronized (state) {
|
||||
String token = call.argument("token");
|
||||
if (held == null || !held.token.equals(token)) {
|
||||
result.error("lock_owner", "Ledger lock token does not match this engine", null);
|
||||
return;
|
||||
}
|
||||
release = held;
|
||||
held = null;
|
||||
}
|
||||
// Never queue release behind another engine's blocking acquisition.
|
||||
release.close();
|
||||
result.success(null);
|
||||
} else {
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
private void acquire(MethodChannel.Result result) {
|
||||
try {
|
||||
LedgerFileLock acquired = LedgerFileLock.acquire(lockFile);
|
||||
synchronized (state) {
|
||||
acquiring = false;
|
||||
if (detached) {
|
||||
acquired.close();
|
||||
return;
|
||||
}
|
||||
held = acquired;
|
||||
}
|
||||
main.post(() -> {
|
||||
synchronized (state) {
|
||||
if (detached || held != acquired) return;
|
||||
result.success(acquired.token);
|
||||
}
|
||||
});
|
||||
} catch (Exception error) {
|
||||
synchronized (state) { acquiring = false; }
|
||||
main.post(() -> {
|
||||
synchronized (state) {
|
||||
if (!detached) result.error("lock_failed", "Unable to acquire the local ledger lock", null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromEngine(FlutterPluginBinding binding) {
|
||||
LedgerFileLock release;
|
||||
synchronized (state) {
|
||||
detached = true;
|
||||
release = held;
|
||||
held = null;
|
||||
if (executor != null) executor.shutdownNow();
|
||||
}
|
||||
if (channel != null) channel.setMethodCallHandler(null);
|
||||
if (release != null) release.close();
|
||||
}
|
||||
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.balance.runtime;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/** OS lock plus JVM serialization, independent of any Flutter engine lifetime. */
|
||||
final class LedgerFileLock implements AutoCloseable {
|
||||
// FileChannel alone throws OverlappingFileLockException in one JVM.
|
||||
private static final Semaphore PROCESS_LOCK = new Semaphore(1, true);
|
||||
final String token = UUID.randomUUID().toString();
|
||||
private final RandomAccessFile file;
|
||||
private final FileChannel channel;
|
||||
private final FileLock lock;
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
|
||||
private LedgerFileLock(RandomAccessFile file, FileChannel channel, FileLock lock) {
|
||||
this.file = file;
|
||||
this.channel = channel;
|
||||
this.lock = lock;
|
||||
}
|
||||
|
||||
static LedgerFileLock acquire(File path) throws IOException, InterruptedException {
|
||||
PROCESS_LOCK.acquire();
|
||||
RandomAccessFile file = null;
|
||||
FileChannel channel = null;
|
||||
FileLock lock = null;
|
||||
try {
|
||||
file = new RandomAccessFile(path, "rw");
|
||||
channel = file.getChannel();
|
||||
lock = channel.lock();
|
||||
return new LedgerFileLock(file, channel, lock);
|
||||
} catch (IOException | RuntimeException error) {
|
||||
try { if (lock != null) lock.release(); } catch (IOException ignored) {}
|
||||
try { if (channel != null) channel.close(); } catch (IOException ignored) {}
|
||||
try { if (file != null) file.close(); } catch (IOException ignored) {}
|
||||
PROCESS_LOCK.release();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed.compareAndSet(false, true)) return;
|
||||
try { lock.release(); } catch (IOException ignored) {}
|
||||
try { channel.close(); } catch (IOException ignored) {}
|
||||
try { file.close(); } catch (IOException ignored) {}
|
||||
PROCESS_LOCK.release();
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.balance.runtime;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import java.io.File;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.Test;
|
||||
|
||||
public class LedgerFileLockTest {
|
||||
@Test
|
||||
public void separateProcessWaitsForOsFileLock() throws Exception {
|
||||
File path = Files.createTempFile("balance-process-lock", ".lock").toFile();
|
||||
Process child = null;
|
||||
try (LedgerFileLock first = LedgerFileLock.acquire(path)) {
|
||||
String classpath = new File(LedgerLockProbe.class.getProtectionDomain()
|
||||
.getCodeSource().getLocation().toURI()).getPath()
|
||||
+ File.pathSeparator + new File(LedgerFileLock.class.getProtectionDomain()
|
||||
.getCodeSource().getLocation().toURI()).getPath();
|
||||
child = new ProcessBuilder(new File(System.getProperty("java.home"), "bin/java").getPath(),
|
||||
"-cp", classpath, LedgerLockProbe.class.getName(), path.getPath())
|
||||
.redirectErrorStream(true).start();
|
||||
BufferedReader output = new BufferedReader(new InputStreamReader(child.getInputStream()));
|
||||
assertEquals("waiting", output.readLine());
|
||||
assertFalse(child.waitFor(100, TimeUnit.MILLISECONDS));
|
||||
first.close();
|
||||
assertTrue(child.waitFor(3, TimeUnit.SECONDS));
|
||||
assertEquals("acquired", output.readLine());
|
||||
assertEquals(0, child.exitValue());
|
||||
} finally {
|
||||
if (child != null && child.isAlive()) child.destroyForcibly();
|
||||
path.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentOwnersWaitAndReleaseIsIdempotent() throws Exception {
|
||||
File path = Files.createTempFile("balance-lock", ".lock").toFile();
|
||||
LedgerFileLock first = LedgerFileLock.acquire(path);
|
||||
CountDownLatch waiting = new CountDownLatch(1);
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
Thread second = new Thread(() -> {
|
||||
waiting.countDown();
|
||||
try (LedgerFileLock ignored = LedgerFileLock.acquire(path)) {
|
||||
entered.countDown();
|
||||
} catch (Exception error) { throw new AssertionError(error); }
|
||||
});
|
||||
second.start();
|
||||
assertTrue(waiting.await(2, TimeUnit.SECONDS));
|
||||
assertFalse(entered.await(100, TimeUnit.MILLISECONDS));
|
||||
first.close();
|
||||
first.close();
|
||||
assertTrue(entered.await(2, TimeUnit.SECONDS));
|
||||
second.join(2000);
|
||||
assertFalse(second.isAlive());
|
||||
path.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptedWaiterDoesNotLeakOrReleaseAnotherOwner() throws Exception {
|
||||
File path = Files.createTempFile("balance-lock", ".lock").toFile();
|
||||
LedgerFileLock first = LedgerFileLock.acquire(path);
|
||||
CountDownLatch waiting = new CountDownLatch(1);
|
||||
AtomicBoolean interrupted = new AtomicBoolean();
|
||||
Thread waiter = new Thread(() -> {
|
||||
waiting.countDown();
|
||||
try (LedgerFileLock ignored = LedgerFileLock.acquire(path)) {
|
||||
fail("Interrupted waiter must not acquire");
|
||||
} catch (InterruptedException expected) { interrupted.set(true); }
|
||||
catch (Exception error) { throw new AssertionError(error); }
|
||||
});
|
||||
waiter.start();
|
||||
assertTrue(waiting.await(2, TimeUnit.SECONDS));
|
||||
waiter.interrupt();
|
||||
waiter.join(2000);
|
||||
assertTrue(interrupted.get());
|
||||
first.close();
|
||||
try (LedgerFileLock after = LedgerFileLock.acquire(path)) {
|
||||
assertNotNull(after.token);
|
||||
}
|
||||
path.delete();
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.balance.runtime;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/** Separate JVM helper used to exercise the actual OS-level file lock. */
|
||||
public final class LedgerLockProbe {
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("waiting");
|
||||
System.out.flush();
|
||||
try (LedgerFileLock ignored = LedgerFileLock.acquire(new File(args[0]))) {
|
||||
System.out.println("acquired");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/// The Android plugin registers the balance/runtime channel in every engine.
|
||||
library;
|
||||
@@ -0,0 +1,16 @@
|
||||
name: balance_runtime
|
||||
description: Shared Android ledger lock for foreground and background Flutter engines.
|
||||
version: 1.0.0
|
||||
publish_to: none
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
flutter: '>=3.22.0'
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
package: com.balance.runtime
|
||||
pluginClass: BalanceRuntimePlugin
|
||||
Reference in new issue
Block a user