126 lines
3.8 KiB
Dart
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',
|
|
];
|
|
}
|
|
}
|