Compare commits
13
Commits
main
...
issue-192-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b93a59537 | ||
|
|
72b25c87ac | ||
|
|
931186dc45 | ||
|
|
5abcf55aa7 | ||
|
|
68dcee6968 | ||
|
|
2a92c8766f | ||
|
|
49ad2ff25d | ||
|
|
c487714b63 | ||
|
|
f560d9d921 | ||
|
|
9eba422c67 | ||
|
|
e7d61e8ee1 | ||
|
|
0e9d7c907e | ||
|
|
ae70646ed4 |
@@ -30,11 +30,44 @@ jobs:
|
|||||||
DAGGER_CLIENT_KEY: ${{ secrets.DAGGER_CLIENT_KEY }}
|
DAGGER_CLIENT_KEY: ${{ secrets.DAGGER_CLIENT_KEY }}
|
||||||
run: scripts/setup_dagger_remote.sh
|
run: scripts/setup_dagger_remote.sh
|
||||||
|
|
||||||
|
- name: Locate Docker daemon for local Dagger engine
|
||||||
|
run: |
|
||||||
|
# Skip if remote Dagger engine is already configured (preferred path)
|
||||||
|
if [ -n "${_DAGGER_RUNNER_HOST:-}" ]; then
|
||||||
|
echo "Remote Dagger engine configured, no local Docker needed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try host Docker socket (DooD) if runner mounts it
|
||||||
|
if [ -S /var/run/docker.sock ]; then
|
||||||
|
if DOCKER_HOST=unix:///var/run/docker.sock docker info >/dev/null 2>&1; then
|
||||||
|
echo "Docker available via host socket."
|
||||||
|
echo "DOCKER_HOST=unix:///var/run/docker.sock" >> "$GITHUB_ENV"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "WARNING: No remote Dagger engine and no local Docker found." >&2
|
||||||
|
echo " - Remote engine: check DAGGER_STUNNEL_URL secret and that the host proxy is running." >&2
|
||||||
|
echo " - Local Docker: runner does not expose /var/run/docker.sock." >&2
|
||||||
|
echo "CI will likely fail at the Dagger step." >&2
|
||||||
|
|
||||||
|
- name: Prune Dagger cache before check
|
||||||
|
env:
|
||||||
|
DAGGER_NO_NAG: "1"
|
||||||
|
run: dagger query '{ engine { localCache { prune } } }' 2>/dev/null || true
|
||||||
|
|
||||||
- name: Run Full Check Suite
|
- name: Run Full Check Suite
|
||||||
env:
|
env:
|
||||||
DAGGER_NO_NAG: "1"
|
DAGGER_NO_NAG: "1"
|
||||||
run: task check-dagger
|
run: task check-dagger
|
||||||
|
|
||||||
|
- name: Prune Dagger cache after check
|
||||||
|
if: always()
|
||||||
|
env:
|
||||||
|
DAGGER_NO_NAG: "1"
|
||||||
|
run: dagger query '{ engine { localCache { prune } } }' 2>/dev/null || true
|
||||||
|
|
||||||
- name: Cleanup TLS credentials
|
- name: Cleanup TLS credentials
|
||||||
if: always()
|
if: always()
|
||||||
run: rm -rf /tmp/dagger-tls /tmp/stunnel-dagger.conf /tmp/stunnel.pid
|
run: rm -rf /tmp/dagger-tls /tmp/stunnel-dagger.conf /tmp/stunnel.pid
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ jobs:
|
|||||||
run: scripts/setup_dagger_remote.sh
|
run: scripts/setup_dagger_remote.sh
|
||||||
|
|
||||||
- name: Run Android Tests on Firebase Test Lab
|
- name: Run Android Tests on Firebase Test Lab
|
||||||
|
if: ${{ secrets.FIREBASE_TEST_LAB_SERVICE_ACCOUNT_KEY != '' }}
|
||||||
env:
|
env:
|
||||||
FIREBASE_TEST_LAB_SERVICE_ACCOUNT_KEY: ${{ secrets.FIREBASE_TEST_LAB_SERVICE_ACCOUNT_KEY }}
|
FIREBASE_TEST_LAB_SERVICE_ACCOUNT_KEY: ${{ secrets.FIREBASE_TEST_LAB_SERVICE_ACCOUNT_KEY }}
|
||||||
FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }}
|
FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }}
|
||||||
@@ -66,6 +67,7 @@ jobs:
|
|||||||
run: scripts/setup_dagger_remote.sh
|
run: scripts/setup_dagger_remote.sh
|
||||||
|
|
||||||
- name: Publish Android to Play Store
|
- name: Publish Android to Play Store
|
||||||
|
if: ${{ secrets.PLAY_STORE_CONFIG_JSON != '' }}
|
||||||
env:
|
env:
|
||||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
|||||||
+6
-1
@@ -284,8 +284,13 @@ tasks:
|
|||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
run_dagger "$@" && return 0
|
run_dagger "$@" && return 0
|
||||||
RC=$?
|
RC=$?
|
||||||
if [ "$attempt" -lt 3 ] && grep -qE "connection reset|context canceled|connection refused" "$DAGGER_OUT"; then
|
if [ "$attempt" -lt 3 ] && grep -qE "connection reset|context canceled|connection refused|invalid return status code" "$DAGGER_OUT"; then
|
||||||
echo "$(_ts) dagger: network error on attempt $attempt/3, retrying..." >&2
|
echo "$(_ts) dagger: network error on attempt $attempt/3, retrying..." >&2
|
||||||
|
elif [ "$attempt" -lt 3 ] && grep -q "No space left on device" "$DAGGER_OUT"; then
|
||||||
|
echo "$(_ts) dagger: disk space error on attempt $attempt/3, pruning Dagger cache..." >&2
|
||||||
|
dagger query '{ engine { localCache { prune } } }' 2>/dev/null || true
|
||||||
|
echo "$(_ts) dagger: waiting 90s for freed space to settle..." >&2
|
||||||
|
sleep 90
|
||||||
else
|
else
|
||||||
return "$RC"
|
return "$RC"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:drift/drift.dart';
|
|||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:enough_mail/enough_mail.dart' as imap;
|
import 'package:enough_mail/enough_mail.dart' as imap;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
@@ -24,6 +25,9 @@ const _kResourceType = 'background_check';
|
|||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void callbackDispatcher() {
|
void callbackDispatcher() {
|
||||||
|
// Required so that path_provider and other plugins are available in this
|
||||||
|
// background isolate (issue #192).
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
Workmanager().executeTask((_, __) async {
|
Workmanager().executeTask((_, __) async {
|
||||||
try {
|
try {
|
||||||
await _doBackgroundSync();
|
await _doBackgroundSync();
|
||||||
|
|||||||
@@ -609,6 +609,17 @@ Future<String> _resolveDatabasePath() async {
|
|||||||
await Future<void>.delayed(Duration(milliseconds: ms));
|
await Future<void>.delayed(Duration(milliseconds: ms));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// On Android, path_provider can be permanently broken on some devices
|
||||||
|
// regardless of how long we wait (issue #192). Derive the path from
|
||||||
|
// /proc/self/cmdline (the Android process name == package name) without
|
||||||
|
// a platform channel as a last resort so the app can still open its DB.
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final fallback = await _androidFallbackPath();
|
||||||
|
if (fallback != null) {
|
||||||
|
_dbPath = fallback;
|
||||||
|
return _dbPath!;
|
||||||
|
}
|
||||||
|
}
|
||||||
throw PlatformException(
|
throw PlatformException(
|
||||||
code: 'channel-error',
|
code: 'channel-error',
|
||||||
message: 'path_provider unavailable after ${delays.length + 1} attempts — '
|
message: 'path_provider unavailable after ${delays.length + 1} attempts — '
|
||||||
@@ -616,10 +627,44 @@ Future<String> _resolveDatabasePath() async {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// These two functions are only called from unit tests (database_path_test.dart).
|
// Reads /proc/self/cmdline to extract the Android package name, then
|
||||||
|
// constructs the standard app files-dir path without a platform channel.
|
||||||
|
// Returns null when the path cannot be determined or created.
|
||||||
|
Future<String?> _androidFallbackPath() async {
|
||||||
|
try {
|
||||||
|
final bytes = await File('/proc/self/cmdline').readAsBytes();
|
||||||
|
final end = bytes.indexOf(0);
|
||||||
|
final packageName = String.fromCharCodes(
|
||||||
|
end >= 0 ? bytes.sublist(0, end) : bytes,
|
||||||
|
).trim();
|
||||||
|
// A valid Android package name contains dots but not slashes.
|
||||||
|
if (packageName.isEmpty ||
|
||||||
|
!packageName.contains('.') ||
|
||||||
|
packageName.contains('/')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (final base in [
|
||||||
|
'/data/user/0/$packageName/files',
|
||||||
|
'/data/data/$packageName/files',
|
||||||
|
]) {
|
||||||
|
try {
|
||||||
|
await Directory(base).create(recursive: true);
|
||||||
|
return p.join(base, 'sharedinbox.db');
|
||||||
|
} catch (_) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// These functions are only called from unit tests (database_path_test.dart).
|
||||||
// They expose internals that cannot be reached via the public API.
|
// They expose internals that cannot be reached via the public API.
|
||||||
Future<String> resolveDatabasePathForTesting() => _resolveDatabasePath();
|
Future<String> resolveDatabasePathForTesting() => _resolveDatabasePath();
|
||||||
void resetDatabasePathForTesting() => _dbPath = null;
|
void resetDatabasePathForTesting() => _dbPath = null;
|
||||||
|
Future<String?> androidFallbackPathForTesting() => _androidFallbackPath();
|
||||||
|
|
||||||
LazyDatabase _openConnection() {
|
LazyDatabase _openConnection() {
|
||||||
return LazyDatabase(() async {
|
return LazyDatabase(() async {
|
||||||
|
|||||||
@@ -14,14 +14,42 @@ if [ "$host" == "$port" ]; then
|
|||||||
port="8774"
|
port="8774"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Probing $host:$port..."
|
MAX_PROBE_ATTEMPTS=5
|
||||||
if ! nc -zw 3 "$host" "$port" 2>/dev/null; then
|
PROBE_DELAY=30
|
||||||
echo "Error: No Dagger server responded on $host:$port"
|
for attempt in $(seq 1 $MAX_PROBE_ATTEMPTS); do
|
||||||
exit 1
|
echo "Probing $host:$port (attempt $attempt/$MAX_PROBE_ATTEMPTS)..."
|
||||||
fi
|
if nc -zw 5 "$host" "$port" 2>/dev/null; then
|
||||||
echo "Found active Dagger server on $host:$port"
|
echo "Found active server on $host:$port"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$attempt" -eq "$MAX_PROBE_ATTEMPTS" ]; then
|
||||||
|
echo "Warning: No Dagger server responded on $host:$port after $MAX_PROBE_ATTEMPTS attempts"
|
||||||
|
echo "Remote engine unavailable — CI will use the local Dagger engine."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Dagger server not responding, waiting ${PROBE_DELAY}s before retry..."
|
||||||
|
sleep $PROBE_DELAY
|
||||||
|
done
|
||||||
|
|
||||||
# 2. Setup TLS credentials (passed as env vars from secrets)
|
# 2a. Try plain TCP connection first (works when server is a plain TCP proxy, no TLS)
|
||||||
|
echo "Trying plain TCP Dagger connection at tcp://$host:$port..."
|
||||||
|
if _DAGGER_RUNNER_HOST="tcp://$host:$port" \
|
||||||
|
_EXPERIMENTAL_DAGGER_RUNNER_HOST="tcp://$host:$port" \
|
||||||
|
timeout 8 dagger version >/dev/null 2>&1; then
|
||||||
|
echo "Plain TCP Dagger connection succeeded — no TLS stunnel needed."
|
||||||
|
if [ -n "${GITHUB_ENV:-}" ]; then
|
||||||
|
echo "_EXPERIMENTAL_DAGGER_RUNNER_HOST=tcp://$host:$port" >> "$GITHUB_ENV"
|
||||||
|
echo "_DAGGER_RUNNER_HOST=tcp://$host:$port" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
export _EXPERIMENTAL_DAGGER_RUNNER_HOST="tcp://$host:$port"
|
||||||
|
export _DAGGER_RUNNER_HOST="tcp://$host:$port"
|
||||||
|
echo "Dagger configured at tcp://$host:$port (plain TCP)"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Plain TCP connection not available; trying TLS stunnel..."
|
||||||
|
|
||||||
|
# 2b. Setup TLS credentials (passed as env vars from secrets)
|
||||||
mkdir -p /tmp/dagger-tls
|
mkdir -p /tmp/dagger-tls
|
||||||
echo "$DAGGER_CA_CERT" > /tmp/dagger-tls/ca.crt
|
echo "$DAGGER_CA_CERT" > /tmp/dagger-tls/ca.crt
|
||||||
echo "$DAGGER_CLIENT_CERT" > /tmp/dagger-tls/client.crt
|
echo "$DAGGER_CLIENT_CERT" > /tmp/dagger-tls/client.crt
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fake_async/fake_async.dart';
|
import 'package:fake_async/fake_async.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -129,5 +130,27 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// The Android fallback runs only on Android, so on the host machine the
|
||||||
|
// exception is still thrown after all retries. Skip on Android to avoid
|
||||||
|
// depending on /data/user/0/... being absent in the test environment.
|
||||||
|
skip: Platform.isAndroid,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Regression test for issue #192: _androidFallbackPath must return null when
|
||||||
|
// the process cmdline does not look like an Android package name (e.g. on
|
||||||
|
// the host test machine where the process is the Dart executable).
|
||||||
|
test(
|
||||||
|
'_androidFallbackPath returns null when process name is not a package name',
|
||||||
|
() async {
|
||||||
|
// On non-Android platforms the host process cmdline is a file-system path
|
||||||
|
// (starts with '/'), which the fallback correctly rejects. On Android
|
||||||
|
// the process IS named after the package — the fallback is free to
|
||||||
|
// succeed or return null depending on the device state; we do not assert
|
||||||
|
// here so as not to constrain Android behaviour.
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
final result = await androidFallbackPathForTesting();
|
||||||
|
expect(result, isNull);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user