mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-11 19:34:02 +01:00
Wires the shared gate (~/utils/scripts/check_file_length.sh) into a new .pre-commit-config.yaml as a local hook, checking only the files in the commit and failing rather than warning. No baseline, no allowlist. The repo had a hand-written .git/hooks/pre-commit running two checks; both are ported verbatim into the config rather than dropped, with the version bump ordered last so it cannot rewrite pubspec.yaml for a commit a gate is still about to reject. scripts/install_hooks.sh now installs pre-commit (and removes the legacy hook) instead of writing its own. .github/workflows/file-length.yml runs the same check on push, kept separate from release-apk.yml so an over-long file fails lint rather than blocking an APK release. Splits the four cleanest violations, all behaviour-preserving: note_template.dart 370 -> 203 + parser (186) firebase_backend.dart 308 -> barrel (24) + project/store/client port_guard.dart 315 -> 232 + linux_system_probe (100) notes_list_screen.dart 698 -> 225 + 5 widget/nav files Where a file had external callers, the original path stays as a barrel that re-exports the split parts, so no call site changed. Verified: 313 tests pass, coverage identical at 99.9% / 1922 lines (per-file too -- notes_list's 281 covered lines are now 81+75+51+41+22+11), flutter analyze clean, and a staged 260-line file makes git commit fail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
3.1 KiB
Dart
101 lines
3.1 KiB
Dart
/// The real-host [SystemProbe]: `ss` for the listener, `/proc` for the rest.
|
|
///
|
|
/// Split out of `port_guard.dart` for file size, and it is the natural seam:
|
|
/// everything here touches the live host, while `port_guard.dart` keeps the
|
|
/// pure decision logic that tests drive with a fake probe.
|
|
library;
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:todo/desktop/port_guard.dart';
|
|
|
|
/// NUL separates the arguments inside `/proc/<pid>/cmdline`.
|
|
final String _argumentSeparator = String.fromCharCode(0);
|
|
|
|
/// [SystemProbe] backed by `ss` and `/proc`.
|
|
///
|
|
/// The desktop wrapper only ever runs on Linux — the desktop app is the web
|
|
/// build served locally — so reading `/proc` directly is safe here.
|
|
class LinuxSystemProbe implements SystemProbe {
|
|
/// Creates the default host probe.
|
|
///
|
|
/// [ssExecutable] exists so a test can point at a missing binary and exercise
|
|
/// the "no `ss` on PATH" path.
|
|
const LinuxSystemProbe({this.ssExecutable = 'ss'});
|
|
|
|
/// Name of the socket-listing tool used to find the port owner.
|
|
final String ssExecutable;
|
|
|
|
@override
|
|
Future<int?> listenerPid(int port) async {
|
|
try {
|
|
final result = await Process.run(ssExecutable, ['-tlnp']);
|
|
final out = result.stdout;
|
|
return out is String ? parseListenerPid(out, port) : null;
|
|
} on ProcessException {
|
|
// No `ss` on PATH: report "unknown" and let the caller fail closed.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<ProcessDescription?> describe(int pid) async {
|
|
final cmdline = readCmdline(pid);
|
|
if (cmdline == null) return null;
|
|
String exe;
|
|
try {
|
|
exe = Link('/proc/$pid/exe').targetSync();
|
|
} on FileSystemException {
|
|
// Unreadable for another user's process; the cmdline still identifies it.
|
|
exe = '';
|
|
}
|
|
return ProcessDescription(exeTarget: exe, cmdline: cmdline);
|
|
}
|
|
|
|
@override
|
|
Future<List<List<String>>> processCmdlines() async {
|
|
final cmdlines = <List<String>>[];
|
|
for (final entry in Directory('/proc').listSync()) {
|
|
final pid = int.tryParse(p.basename(entry.path));
|
|
if (pid == null) continue;
|
|
final cmdline = readCmdline(pid);
|
|
if (cmdline != null) cmdlines.add(cmdline);
|
|
}
|
|
return cmdlines;
|
|
}
|
|
|
|
@override
|
|
Future<bool> portInUse(int port) async {
|
|
try {
|
|
final socket = await ServerSocket.bind(
|
|
InternetAddress.loopbackIPv4,
|
|
port,
|
|
);
|
|
await socket.close();
|
|
return false;
|
|
} on SocketException {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool signal(int pid, ProcessSignal signal) => Process.killPid(pid, signal);
|
|
|
|
/// Reads `/proc/<pid>/cmdline` as an argument vector.
|
|
///
|
|
/// Returns null when the process is gone. Kernel threads yield an empty list,
|
|
/// and the trailing separator is dropped so exact-token matching holds.
|
|
static List<String>? readCmdline(int pid) {
|
|
try {
|
|
return File('/proc/$pid/cmdline')
|
|
.readAsStringSync()
|
|
.split(_argumentSeparator)
|
|
.where((token) => token.isNotEmpty)
|
|
.toList();
|
|
} on FileSystemException {
|
|
return null;
|
|
}
|
|
}
|
|
}
|