mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 19:27:01 +01:00
Desktop: launch Chrome with --password-store=basic so a locked keyring cannot blank the window
Some checks failed
Some checks failed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8aa365919b
commit
2bbfe0b663
@ -117,8 +117,8 @@ rewrites links to `~/data/cloud/todo-images/…` so an agent can open them.
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- **The suite must stay quick and fully green.** It is currently **418 tests at
|
- **The suite must stay quick and fully green.** It is currently **427 tests at
|
||||||
100% line coverage** (2494 lines), ~30s on this machine with
|
100% line coverage** (2512 lines), ~30s on this machine with
|
||||||
`--concurrency=3` (unbounded concurrency under `capped.sh`'s 4 GiB gets its
|
`--concurrency=3` (unbounded concurrency under `capped.sh`'s 4 GiB gets its
|
||||||
test processes SIGTERMed while the wrapper still exits 0 — check the last
|
test processes SIGTERMed while the wrapper still exits 0 — check the last
|
||||||
line says "All tests passed"). Don't regress either.
|
line says "All tests passed"). Don't regress either.
|
||||||
|
|||||||
@ -24,12 +24,14 @@ String profileDir(String home) =>
|
|||||||
p.join(home, '.local', 'share', 'todo-desktop', 'profile');
|
p.join(home, '.local', 'share', 'todo-desktop', 'profile');
|
||||||
|
|
||||||
/// Returns the first Chrome-family browser on this machine, or ''.
|
/// 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
|
// Ordered by preference, and deliberately broad: this machine runs Thorium
|
||||||
// behind /opt/google/chrome, and has a policy that uninstalls the `chromium`
|
// behind /opt/google/chrome, and has a policy that uninstalls the `chromium`
|
||||||
// package, so assuming any single browser is wrong. TODO_BROWSER overrides.
|
// package, so assuming any single browser is wrong. TODO_BROWSER overrides.
|
||||||
final candidates = [
|
final candidates = [
|
||||||
Platform.environment['TODO_BROWSER'] ?? '',
|
override ?? Platform.environment['TODO_BROWSER'] ?? '',
|
||||||
'/opt/google/chrome/chrome',
|
'/opt/google/chrome/chrome',
|
||||||
'/opt/thorium-browser/thorium-browser',
|
'/opt/thorium-browser/thorium-browser',
|
||||||
'/usr/bin/google-chrome-stable',
|
'/usr/bin/google-chrome-stable',
|
||||||
@ -37,17 +39,40 @@ String findBrowser() {
|
|||||||
'/usr/bin/brave',
|
'/usr/bin/brave',
|
||||||
];
|
];
|
||||||
return candidates.firstWhere(
|
return candidates.firstWhere(
|
||||||
(path) => path.isNotEmpty && File(path).existsSync(),
|
(path) => path.isNotEmpty && (exists ?? (p) => File(p).existsSync())(path),
|
||||||
orElse: () => '',
|
orElse: () => '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
// Without it the window inherits the browser's class and the taskbar shows
|
||||||
|
// 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
|
/// Launches the app in a Chrome-family browser with a **stable** profile
|
||||||
/// directory, since the token and notes live in that profile.
|
/// directory, since the token and notes live in that profile.
|
||||||
///
|
///
|
||||||
/// Returns true when the browser ran long enough to have owned the session.
|
/// Returns true when the browser ran long enough to have owned the session.
|
||||||
Future<bool> launchBrowser(String home) async {
|
/// [browser] and [start] exist for tests; they default to [findBrowser] and
|
||||||
final browser = findBrowser();
|
/// [Process.start].
|
||||||
|
Future<bool> launchBrowser(
|
||||||
|
String home, {
|
||||||
|
String? browser,
|
||||||
|
Future<Process> Function(String executable, List<String> args)? start,
|
||||||
|
}) async {
|
||||||
|
browser ??= findBrowser();
|
||||||
if (browser.isEmpty) {
|
if (browser.isEmpty) {
|
||||||
stderr.writeln(
|
stderr.writeln(
|
||||||
'No Chrome-family browser found; open '
|
'No Chrome-family browser found; open '
|
||||||
@ -55,15 +80,7 @@ Future<bool> launchBrowser(String home) async {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final process = await Process.start(browser, [
|
final process = await (start ?? Process.start)(browser, browserArgs(home));
|
||||||
'--app=http://localhost:$kWrapperPort',
|
|
||||||
'--user-data-dir=${profileDir(home)}',
|
|
||||||
// Sets WM_CLASS, which the .desktop entry matches on via StartupWMClass.
|
|
||||||
// Without it the window inherits the browser's class and the taskbar shows
|
|
||||||
// a browser icon instead of todo's.
|
|
||||||
'--class=todo',
|
|
||||||
'--no-first-run',
|
|
||||||
]);
|
|
||||||
|
|
||||||
final started = DateTime.now();
|
final started = DateTime.now();
|
||||||
await process.exitCode;
|
await process.exitCode;
|
||||||
|
|||||||
@ -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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: ^3.12.2
|
sdk: ^3.12.2
|
||||||
|
|||||||
96
test/desktop/browser_launcher_test.dart
Normal file
96
test/desktop/browser_launcher_test.dart
Normal 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');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user