Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
lib/Peach/see/systemd/ConnectSystemd.dart
Normal file
6
lib/Peach/see/systemd/ConnectSystemd.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import '../../comms/Comms.dart' show DeployResult;
|
||||
|
||||
abstract class ConnectSystemd {
|
||||
/// The unit was written and registered (or failed to be).
|
||||
void onUnitInstalled(DeployResult result);
|
||||
}
|
||||
20
lib/Peach/see/systemd/Systemd.dart
Normal file
20
lib/Peach/see/systemd/Systemd.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../about/internal/application/ServerProfile.dart';
|
||||
import '../../about/internal/application/ServiceConfig.dart';
|
||||
import 'SystemdState.dart';
|
||||
|
||||
/// Step-by-step builder that generates a systemd `.service` unit, previews it,
|
||||
/// then registers it (daemon-reload → enable → start → status).
|
||||
class Systemd extends StatefulWidget {
|
||||
final ServerProfile server;
|
||||
|
||||
/// Prefills the form and, once the unit registers, has its `serviceName`
|
||||
/// updated so deploys and log views use the new unit.
|
||||
final ServiceConfig? service;
|
||||
|
||||
const Systemd({super.key, required this.server, this.service});
|
||||
|
||||
@override
|
||||
State<Systemd> createState() => SystemdState();
|
||||
}
|
||||
583
lib/Peach/see/systemd/SystemdState.dart
Normal file
583
lib/Peach/see/systemd/SystemdState.dart
Normal file
@@ -0,0 +1,583 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../about/external/initial/SystemdSpec.dart';
|
||||
import '../../about/internal/application/ServerProfile.dart';
|
||||
import '../../about/internal/application/StepTracker.dart';
|
||||
import '../../about/internal/application/TextType.dart';
|
||||
import '../../comms/Comms.dart' show DeployResult, LogLine;
|
||||
import '../../designs/LogConsole.dart';
|
||||
import '../../designs/StepList.dart';
|
||||
import '../../designs/text/Text.dart';
|
||||
import '../../utils/Colors.dart';
|
||||
import 'ConnectSystemd.dart';
|
||||
import 'Systemd.dart';
|
||||
import 'ViewSystemd.dart';
|
||||
|
||||
class SystemdState extends State<Systemd> implements ConnectSystemd {
|
||||
static const _dependencyOptions = {
|
||||
'mariadb.service': 'MariaDB',
|
||||
'mysql.service': 'MySQL',
|
||||
'postgresql.service': 'PostgreSQL',
|
||||
'docker.service': 'Docker',
|
||||
'redis.service': 'Redis',
|
||||
'nginx.service': 'nginx',
|
||||
};
|
||||
|
||||
ViewSystemd? _model;
|
||||
|
||||
StreamSubscription<LogLine>? _logSub;
|
||||
StreamSubscription<List<DeployStep>>? _stepSub;
|
||||
|
||||
final _lines = <LogLine>[];
|
||||
List<DeployStep> _steps = [];
|
||||
bool _applying = false;
|
||||
DeployResult? _result;
|
||||
|
||||
late final TextEditingController _name;
|
||||
late final TextEditingController _description;
|
||||
late final TextEditingController _user;
|
||||
late final TextEditingController _workDir;
|
||||
late final TextEditingController _exec;
|
||||
late final TextEditingController _restartSec;
|
||||
late final TextEditingController _environment;
|
||||
|
||||
SystemdType _type = SystemdType.simple;
|
||||
RestartPolicy _restart = RestartPolicy.onFailure;
|
||||
final Set<String> _after = {};
|
||||
bool _javaExit = false;
|
||||
bool _enableOnBoot = true;
|
||||
bool _startNow = true;
|
||||
|
||||
ServerProfile get _server => widget.server;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final service = widget.service;
|
||||
final isJar = service != null && !service.isWeb;
|
||||
final jarPath = service == null
|
||||
? '/opt/app/app.jar'
|
||||
: service.remotePath(
|
||||
service.remoteJarName.trim().isEmpty
|
||||
? 'app.jar'
|
||||
: service.remoteJarName,
|
||||
);
|
||||
|
||||
_name = TextEditingController(
|
||||
text: service?.serviceName.trim().isNotEmpty == true
|
||||
? service!.serviceName.trim()
|
||||
: (service?.name ?? 'myapp'),
|
||||
);
|
||||
_description = TextEditingController(
|
||||
text: 'Peach · ${service?.name ?? 'service'}',
|
||||
);
|
||||
_user = TextEditingController(text: _server.username);
|
||||
_workDir = TextEditingController(text: service?.remoteRoot ?? '/opt/app');
|
||||
_exec = TextEditingController(
|
||||
text: isJar ? '/usr/bin/java -jar $jarPath' : '',
|
||||
);
|
||||
_restartSec = TextEditingController(text: '5');
|
||||
_environment = TextEditingController();
|
||||
_javaExit = isJar;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_logSub?.cancel();
|
||||
_stepSub?.cancel();
|
||||
for (final c in [
|
||||
_name,
|
||||
_description,
|
||||
_user,
|
||||
_workDir,
|
||||
_exec,
|
||||
_restartSec,
|
||||
_environment,
|
||||
]) {
|
||||
c.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ViewSystemd>.reactive(
|
||||
viewModelBuilder: () => ViewSystemd(context, this),
|
||||
onViewModelReady: _onReady,
|
||||
builder: (context, viewModel, child) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: text(
|
||||
'Register systemd service',
|
||||
20,
|
||||
TextType.Bold,
|
||||
color: context.jp.text,
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 40),
|
||||
children: [
|
||||
_sectionTitle('1 · Unit', Icons.badge_outlined),
|
||||
_field('Unit name', _name, hint: 'orders → orders.service'),
|
||||
_field('Description', _description),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _typeDropdown()),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _restartDropdown()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_sectionTitle('2 · Process', Icons.terminal),
|
||||
_field('User', _user),
|
||||
_field('Working directory', _workDir, hint: '/opt/app'),
|
||||
_field(
|
||||
'ExecStart',
|
||||
_exec,
|
||||
hint: '/usr/bin/java -jar /opt/app/app.jar',
|
||||
mono: true,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: _field(
|
||||
'RestartSec',
|
||||
_restartSec,
|
||||
keyboard: TextInputType.number,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
value: _javaExit,
|
||||
onChanged: _onJavaExitChanged,
|
||||
title: text(
|
||||
'JVM SuccessExitStatus=143',
|
||||
14,
|
||||
TextType.Regular,
|
||||
color: context.jp.text,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_field(
|
||||
'Environment (KEY=VALUE per line)',
|
||||
_environment,
|
||||
hint: 'SPRING_PROFILES_ACTIVE=prod\nJAVA_OPTS=-Xmx512m',
|
||||
mono: true,
|
||||
lines: 3,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_sectionTitle('3 · Dependencies', Icons.account_tree),
|
||||
text(
|
||||
'Starts after network.target',
|
||||
12,
|
||||
TextType.Regular,
|
||||
color: context.jp.textFaint,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_dependencyChips(),
|
||||
const SizedBox(height: 12),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: _enableOnBoot,
|
||||
onChanged: _onEnableChanged,
|
||||
title: text(
|
||||
'Enable on boot',
|
||||
14,
|
||||
TextType.Regular,
|
||||
color: context.jp.text,
|
||||
),
|
||||
subtitle: text(
|
||||
'systemctl enable',
|
||||
12,
|
||||
TextType.Light,
|
||||
color: context.jp.textDim,
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: _startNow,
|
||||
onChanged: _onStartNowChanged,
|
||||
title: text(
|
||||
'Start now',
|
||||
14,
|
||||
TextType.Regular,
|
||||
color: context.jp.text,
|
||||
),
|
||||
subtitle: text(
|
||||
'systemctl restart after install',
|
||||
12,
|
||||
TextType.Light,
|
||||
color: context.jp.textDim,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_sectionTitle('4 · Preview & confirm', Icons.rule),
|
||||
_previewCard(_spec()),
|
||||
const SizedBox(height: 10),
|
||||
_plannedCommands(_spec()),
|
||||
const SizedBox(height: 14),
|
||||
FilledButton.icon(
|
||||
onPressed: _applying ? null : _onApply,
|
||||
icon: _applying
|
||||
? SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: colorWhite,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.check_circle),
|
||||
label: text(
|
||||
_applying ? 'Registering…' : 'Install & register unit',
|
||||
14,
|
||||
TextType.Bold,
|
||||
color: colorWhite,
|
||||
),
|
||||
),
|
||||
if (_steps.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
StepList(steps: _steps),
|
||||
],
|
||||
if (_result != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
_resultBanner(_result!),
|
||||
],
|
||||
if (_lines.isNotEmpty) ...[
|
||||
const SizedBox(height: 14),
|
||||
LogConsole(
|
||||
logs: _lines,
|
||||
height: 260,
|
||||
onClear: _onClearLogs,
|
||||
onCopy: _onCopyLogs,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onReady(ViewSystemd model) {
|
||||
_model = model;
|
||||
// onViewModelReady fires mid-build; a synchronous setState from the
|
||||
// first load would dirty this State while it is still being built.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _initiate();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initiate() async {
|
||||
_logSub = _model?.logs.listen(_onLogLine);
|
||||
_stepSub = _model?.steps.listen(_onSteps);
|
||||
}
|
||||
|
||||
void _onLogLine(LogLine line) {
|
||||
if (!mounted) return;
|
||||
setState(() => _lines.add(line));
|
||||
}
|
||||
|
||||
void _onSteps(List<DeployStep> steps) {
|
||||
if (!mounted) return;
|
||||
setState(() => _steps = steps);
|
||||
}
|
||||
|
||||
SystemdSpec _spec() => SystemdSpec(
|
||||
unitName: _name.text,
|
||||
description: _description.text,
|
||||
type: _type,
|
||||
user: _user.text,
|
||||
workingDirectory: _workDir.text,
|
||||
execStart: _exec.text,
|
||||
restart: _restart,
|
||||
restartSec: int.tryParse(_restartSec.text) ?? 5,
|
||||
environmentLines: _environment.text,
|
||||
afterUnits: ['network.target', ..._after],
|
||||
javaSuccessExit: _javaExit,
|
||||
enableOnBoot: _enableOnBoot,
|
||||
startNow: _startNow,
|
||||
);
|
||||
|
||||
// ── named handlers ─────────────────────────────────────────────────────────
|
||||
|
||||
void _onApply() {
|
||||
final spec = _spec();
|
||||
if (spec.safeName.isEmpty) {
|
||||
_notify('Enter a unit name.');
|
||||
return;
|
||||
}
|
||||
if (spec.execStart.trim().isEmpty) {
|
||||
_notify('Enter an ExecStart command.');
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_applying = true;
|
||||
_lines.clear();
|
||||
_result = null;
|
||||
});
|
||||
_model?.installUnit(_server, spec, widget.service);
|
||||
}
|
||||
|
||||
void _onTypeChanged(SystemdType? v) =>
|
||||
setState(() => _type = v ?? SystemdType.simple);
|
||||
|
||||
void _onRestartChanged(RestartPolicy? v) =>
|
||||
setState(() => _restart = v ?? RestartPolicy.onFailure);
|
||||
|
||||
void _onJavaExitChanged(bool v) => setState(() => _javaExit = v);
|
||||
|
||||
void _onEnableChanged(bool v) => setState(() => _enableOnBoot = v);
|
||||
|
||||
void _onStartNowChanged(bool v) => setState(() => _startNow = v);
|
||||
|
||||
void _onDependencyChanged(String unit, bool selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_after.add(unit);
|
||||
} else {
|
||||
_after.remove(unit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The preview re-renders from the live field values, so every keystroke
|
||||
/// has to repaint.
|
||||
void _onFieldChanged(String _) => setState(() {});
|
||||
|
||||
void _onCopyUnit(SystemdSpec spec) {
|
||||
Clipboard.setData(ClipboardData(text: spec.render()));
|
||||
_notify('Unit file copied.');
|
||||
}
|
||||
|
||||
void _onClearLogs() => setState(() => _lines.clear());
|
||||
|
||||
void _onCopyLogs() {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: _lines.map((l) => l.text).join('\n')),
|
||||
);
|
||||
_notify('Logs copied.');
|
||||
}
|
||||
|
||||
void _notify(String message) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
|
||||
// ── ConnectSystemd ─────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
void onUnitInstalled(DeployResult result) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_applying = false;
|
||||
_result = result;
|
||||
});
|
||||
}
|
||||
|
||||
// ── screen-only widgets ────────────────────────────────────────────────────
|
||||
|
||||
Widget _sectionTitle(String title, IconData icon) => Padding(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: PeachTheme.seed),
|
||||
const SizedBox(width: 8),
|
||||
text(
|
||||
title.toUpperCase(),
|
||||
12.5,
|
||||
TextType.Bold,
|
||||
color: context.jp.text,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _field(
|
||||
String label,
|
||||
TextEditingController controller, {
|
||||
String? hint,
|
||||
bool mono = false,
|
||||
int lines = 1,
|
||||
TextInputType? keyboard,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
maxLines: lines,
|
||||
keyboardType: keyboard,
|
||||
onChanged: _onFieldChanged,
|
||||
style: mono
|
||||
? const TextStyle(fontFamily: 'monospace', fontSize: 13)
|
||||
: null,
|
||||
decoration: InputDecoration(labelText: label, hintText: hint),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _typeDropdown() => DropdownButtonFormField<SystemdType>(
|
||||
initialValue: _type,
|
||||
decoration: const InputDecoration(labelText: 'Type'),
|
||||
items: SystemdType.values
|
||||
.map((t) => DropdownMenuItem(value: t, child: Text(t.name)))
|
||||
.toList(),
|
||||
onChanged: _onTypeChanged,
|
||||
);
|
||||
|
||||
Widget _restartDropdown() => DropdownButtonFormField<RestartPolicy>(
|
||||
initialValue: _restart,
|
||||
decoration: const InputDecoration(labelText: 'Restart'),
|
||||
items: RestartPolicy.values
|
||||
.map((r) => DropdownMenuItem(value: r, child: Text(r.label)))
|
||||
.toList(),
|
||||
onChanged: _onRestartChanged,
|
||||
);
|
||||
|
||||
Widget _dependencyChips() {
|
||||
final jp = context.jp;
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _dependencyOptions.entries.map((entry) {
|
||||
final on = _after.contains(entry.key);
|
||||
return FilterChip(
|
||||
label: Text(entry.value),
|
||||
selected: on,
|
||||
onSelected: (s) => _onDependencyChanged(entry.key, s),
|
||||
showCheckmark: false,
|
||||
selectedColor: PeachTheme.seed.withValues(alpha: 0.20),
|
||||
backgroundColor: jp.surface2,
|
||||
side: BorderSide(color: on ? PeachTheme.seed : jp.border),
|
||||
labelStyle: TextStyle(
|
||||
color: on ? PeachTheme.seed : jp.textDim,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _previewCard(SystemdSpec spec) {
|
||||
final jp = context.jp;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: jp.consoleBg,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: ConsoleColors.frame),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.description, size: 15, color: PeachTheme.seed),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'/etc/systemd/system/${spec.safeName}.service',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
color: ConsoleColors.header,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Copy',
|
||||
iconSize: 15,
|
||||
color: ConsoleColors.header,
|
||||
onPressed: () => _onCopyUnit(spec),
|
||||
icon: const Icon(Icons.copy_all),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: ConsoleColors.frame),
|
||||
SelectableText(
|
||||
spec.render(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12.5,
|
||||
height: 1.4,
|
||||
color: ConsoleColors.body,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _plannedCommands(SystemdSpec spec) {
|
||||
final jp = context.jp;
|
||||
final commands = spec.plannedCommands(sudo: _server.useSudo);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: PeachTheme.panel(context, radius: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
text(
|
||||
'THESE COMMANDS WILL RUN',
|
||||
11,
|
||||
TextType.Bold,
|
||||
color: jp.textDim,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
for (final command in commands)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.chevron_right, size: 15, color: jp.textFaint),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: text(
|
||||
command,
|
||||
12.5,
|
||||
TextType.Regular,
|
||||
color: jp.textDim,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _resultBanner(DeployResult result) {
|
||||
final color = result.ok ? PeachTheme.seed : PeachTheme.danger;
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 12, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(result.ok ? Icons.check_circle : Icons.error, color: color),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: text(result.message, 14, TextType.Regular, color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
68
lib/Peach/see/systemd/ViewSystemd.dart
Normal file
68
lib/Peach/see/systemd/ViewSystemd.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import '../../about/external/initial/SystemdSpec.dart';
|
||||
import '../../about/internal/application/ServerProfile.dart';
|
||||
import '../../about/internal/application/ServiceConfig.dart';
|
||||
import '../../about/internal/application/StepTracker.dart';
|
||||
import '../../comms/Comms.dart' show LogLine;
|
||||
import '../parent/ParentViewModel.dart';
|
||||
import 'ConnectSystemd.dart';
|
||||
|
||||
class ViewSystemd extends ParentViewModel {
|
||||
ConnectSystemd connection;
|
||||
|
||||
ViewSystemd(super.context, this.connection);
|
||||
|
||||
Stream<LogLine> get logs => getDataManager().logs;
|
||||
|
||||
Stream<List<DeployStep>> get steps => getDataManager().stepStream;
|
||||
|
||||
/// Writes the unit file, reloads systemd and (optionally) enables and starts
|
||||
/// it. On success the unit name is recorded against [service] so the deploy
|
||||
/// and log screens target the right unit from now on.
|
||||
void installUnit(
|
||||
ServerProfile server,
|
||||
SystemdSpec spec,
|
||||
ServiceConfig? service,
|
||||
) async {
|
||||
if (!await canReach(server, () => installUnit(server, spec, service))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final result = await getDataManager().installSystemdUnit(
|
||||
server: server,
|
||||
unitName: spec.safeName,
|
||||
unitContent: spec.render(),
|
||||
enable: spec.enableOnBoot,
|
||||
startNow: spec.startNow,
|
||||
);
|
||||
|
||||
if (result.ok && service != null) {
|
||||
service.serviceName = spec.safeName;
|
||||
await _persistService(server, service);
|
||||
}
|
||||
|
||||
connection.onUnitInstalled(result);
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
() => installUnit(server, spec, service),
|
||||
() {},
|
||||
"Retry",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the service back onto its server in storage.
|
||||
Future<void> _persistService(
|
||||
ServerProfile server,
|
||||
ServiceConfig service,
|
||||
) async {
|
||||
final servers = await getDataManager().getServers();
|
||||
final serverIndex = servers.indexWhere((s) => s.id == server.id);
|
||||
if (serverIndex < 0) return;
|
||||
final services = servers[serverIndex].services;
|
||||
final serviceIndex = services.indexWhere((s) => s.id == service.id);
|
||||
if (serviceIndex < 0) return;
|
||||
services[serviceIndex] = service;
|
||||
await getDataManager().setServers(servers);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user