Desktop: launch Chrome with --password-store=basic so a locked keyring cannot blank the window
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-09-25 16:31:08 +02:00
parent 8aa365919b
commit 2bbfe0b663
4 changed files with 130 additions and 17 deletions

View File

@ -117,8 +117,8 @@ rewrites links to `~/data/cloud/todo-images/…` so an agent can open them.
## Testing
- **The suite must stay quick and fully green.** It is currently **418 tests at
100% line coverage** (2494 lines), ~30s on this machine with
- **The suite must stay quick and fully green.** It is currently **427 tests at
100% line coverage** (2512 lines), ~30s on this machine with
`--concurrency=3` (unbounded concurrency under `capped.sh`'s 4 GiB gets its
test processes SIGTERMed while the wrapper still exits 0 — check the last
line says "All tests passed"). Don't regress either.

View File

@ -24,12 +24,14 @@ String profileDir(String home) =>
p.join(home, '.local', 'share', 'todo-desktop', 'profile');
/// Returns the first Chrome-family browser on this machine, or ''.
String findBrowser() {
///
/// [override] defaults to `$TODO_BROWSER`; [exists] to a file check.
String findBrowser({String? override, bool Function(String path)? exists}) {
// Ordered by preference, and deliberately broad: this machine runs Thorium
// behind /opt/google/chrome, and has a policy that uninstalls the `chromium`
// package, so assuming any single browser is wrong. TODO_BROWSER overrides.
final candidates = [
Platform.environment['TODO_BROWSER'] ?? '',
override ?? Platform.environment['TODO_BROWSER'] ?? '',
'/opt/google/chrome/chrome',
'/opt/thorium-browser/thorium-browser',
'/usr/bin/google-chrome-stable',
@ -37,25 +39,13 @@ String findBrowser() {
'/usr/bin/brave',
];
return candidates.firstWhere(
(path) => path.isNotEmpty && File(path).existsSync(),
(path) => path.isNotEmpty && (exists ?? (p) => File(p).existsSync())(path),
orElse: () => '',
);
}
/// Launches the app in a Chrome-family browser with a **stable** profile
/// directory, since the token and notes live in that profile.
///
/// Returns true when the browser ran long enough to have owned the session.
Future<bool> launchBrowser(String home) async {
final browser = findBrowser();
if (browser.isEmpty) {
stderr.writeln(
'No Chrome-family browser found; open '
'http://localhost:$kWrapperPort manually.',
);
return false;
}
final process = await Process.start(browser, [
/// Command-line arguments for the app window.
List<String> browserArgs(String home) => [
'--app=http://localhost:$kWrapperPort',
'--user-data-dir=${profileDir(home)}',
// Sets WM_CLASS, which the .desktop entry matches on via StartupWMClass.
@ -63,7 +53,34 @@ Future<bool> launchBrowser(String home) async {
// a browser icon instead of todo's.
'--class=todo',
'--no-first-run',
]);
// Keep Chrome off the GNOME keyring. While the keyring is locked, Chrome
// blocks every non-incognito page load on its first cookie access, and an
// "Unlock Keyring" prompt waits on another workspace: the window stays a
// blank grey. Nothing in this profile needs OS-level encryption (the
// GitHub token is in localStorage, not the password store).
'--password-store=basic',
];
/// Launches the app in a Chrome-family browser with a **stable** profile
/// directory, since the token and notes live in that profile.
///
/// Returns true when the browser ran long enough to have owned the session.
/// [browser] and [start] exist for tests; they default to [findBrowser] and
/// [Process.start].
Future<bool> launchBrowser(
String home, {
String? browser,
Future<Process> Function(String executable, List<String> args)? start,
}) async {
browser ??= findBrowser();
if (browser.isEmpty) {
stderr.writeln(
'No Chrome-family browser found; open '
'http://localhost:$kWrapperPort manually.',
);
return false;
}
final process = await (start ?? Process.start)(browser, browserArgs(home));
final started = DateTime.now();
await process.exitCode;

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.38+1
version: 1.0.39+1
environment:
sdk: ^3.12.2

View File

@ -0,0 +1,96 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:todo/desktop/browser_launcher.dart';
void main() {
group('browserArgs', () {
final args = browserArgs('/h');
test('opens the wrapper as an app window in the fixed profile', () {
expect(args, contains('--app=http://localhost:$kWrapperPort'));
expect(
args,
contains('--user-data-dir=/h/.local/share/todo-desktop/profile'),
);
expect(args, contains('--class=todo'));
});
test('keeps Chrome off the keyring, whose lock blanks the window', () {
expect(args, contains('--password-store=basic'));
});
});
group('findBrowser', () {
test('an override that exists wins', () {
expect(findBrowser(override: '/x/b', exists: (_) => true), '/x/b');
});
test('falls through to the first installed candidate', () {
expect(
findBrowser(
override: '',
exists: (p) => p == '/usr/bin/google-chrome-stable',
),
'/usr/bin/google-chrome-stable',
);
});
test("returns '' when nothing is installed", () {
expect(findBrowser(override: '', exists: (_) => false), '');
});
test(r'defaults to $TODO_BROWSER and real files', () {
expect(findBrowser(), isA<String>());
});
});
group('launchBrowser', () {
test('defaults to the browser findBrowser picks', () async {
final expected = findBrowser();
String? exe;
await launchBrowser(
'/h',
start: (e, _) {
exe = e;
return Process.start('true', []);
},
);
expect(exe, expected.isEmpty ? isNull : expected);
});
test('without a browser it reports false and starts nothing', () async {
var started = false;
final ok = await launchBrowser(
'/h',
browser: '',
start: (_, _) async {
started = true;
return Process.start('true', []);
},
);
expect(ok, isFalse);
expect(started, isFalse);
});
test(
'starts the browser with browserArgs; a quick exit is false',
() async {
String? exe;
List<String>? passed;
final ok = await launchBrowser(
'/h',
browser: '/x/chrome',
start: (e, a) {
exe = e;
passed = a;
return Process.start('true', []);
},
);
expect(exe, '/x/chrome');
expect(passed, browserArgs('/h'));
expect(ok, isFalse, reason: 'ran under 5 s: did not own the session');
},
);
});
}