Files
alvocool 38881d1fe1 Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 10:15:30 +03:00

126 lines
3.8 KiB
Dart

/// systemd service [Type].
enum SystemdType { simple, exec, forking, oneshot, notify }
/// systemd Restart= policy.
enum RestartPolicy { onFailure, always, no }
extension RestartPolicyX on RestartPolicy {
String get value => switch (this) {
RestartPolicy.onFailure => 'on-failure',
RestartPolicy.always => 'always',
RestartPolicy.no => 'no',
};
String get label => switch (this) {
RestartPolicy.onFailure => 'On failure',
RestartPolicy.always => 'Always',
RestartPolicy.no => 'Never',
};
}
/// Everything needed to generate and register a `.service` unit file.
class SystemdSpec {
String unitName; // e.g. "orders" -> orders.service
String description;
SystemdType type;
String user;
String group;
String workingDirectory;
String execStart;
RestartPolicy restart;
int restartSec;
/// Raw "KEY=VALUE" lines.
String environmentLines;
/// Extra `After=`/`Wants=` targets, e.g. mariadb.service.
List<String> afterUnits;
/// Add `SuccessExitStatus=143` (SIGTERM) — recommended for JVM apps.
bool javaSuccessExit;
bool enableOnBoot;
bool startNow;
SystemdSpec({
this.unitName = '',
this.description = '',
this.type = SystemdType.simple,
this.user = 'root',
this.group = '',
this.workingDirectory = '',
this.execStart = '',
this.restart = RestartPolicy.onFailure,
this.restartSec = 5,
this.environmentLines = '',
List<String>? afterUnits,
this.javaSuccessExit = false,
this.enableOnBoot = true,
this.startNow = true,
}) : afterUnits = afterUnits ?? ['network.target'];
/// Sanitized unit name (no spaces / .service suffix).
String get safeName {
var n = unitName.trim();
if (n.toLowerCase().endsWith('.service')) {
n = n.substring(0, n.length - '.service'.length);
}
return n.replaceAll(RegExp(r'[^A-Za-z0-9_.@-]'), '-');
}
List<String> get envList => environmentLines
.split('\n')
.map((e) => e.trim())
.where((e) => e.isNotEmpty && e.contains('='))
.toList();
/// Renders the unit file contents.
String render() {
final b = StringBuffer();
b.writeln('# Generated by PeachDeployments');
b.writeln('[Unit]');
b.writeln(
'Description=${description.trim().isEmpty ? safeName : description.trim()}',
);
final afters = afterUnits.where((a) => a.trim().isNotEmpty).toList();
if (afters.isNotEmpty) {
b.writeln('After=${afters.join(' ')}');
// Wants only the non-core targets (don't hard-require network.target).
final wants = afters.where((a) => a != 'network.target').toList();
if (wants.isNotEmpty) b.writeln('Wants=${wants.join(' ')}');
}
b.writeln();
b.writeln('[Service]');
b.writeln('Type=${type.name}');
if (user.trim().isNotEmpty) b.writeln('User=${user.trim()}');
if (group.trim().isNotEmpty) b.writeln('Group=${group.trim()}');
if (workingDirectory.trim().isNotEmpty) {
b.writeln('WorkingDirectory=${workingDirectory.trim()}');
}
for (final e in envList) {
b.writeln('Environment="$e"');
}
b.writeln('ExecStart=${execStart.trim()}');
b.writeln('Restart=${restart.value}');
if (restart != RestartPolicy.no) b.writeln('RestartSec=$restartSec');
if (javaSuccessExit) b.writeln('SuccessExitStatus=143');
b.writeln();
b.writeln('[Install]');
b.writeln('WantedBy=multi-user.target');
return b.toString();
}
/// Human-readable list of the commands the installer will run.
List<String> plannedCommands({required bool sudo}) {
final p = sudo ? 'sudo ' : '';
final target = '/etc/systemd/system/$safeName.service';
return [
'write $target',
'${p}chmod 644 $target',
'${p}systemctl daemon-reload',
if (enableOnBoot) '${p}systemctl enable $safeName',
if (startNow) '${p}systemctl restart $safeName',
'${p}systemctl status $safeName',
];
}
}