Skip to content

Commit 426ffa6

Browse files
committed
refactor: simplify emulator backup script to systemd services
1 parent bcfa1a1 commit 426ffa6

File tree

1 file changed

+161
-193
lines changed

1 file changed

+161
-193
lines changed
Lines changed: 161 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,130 @@
1-
# switch.nix
1+
# switch.nix - Systemd-based backup service for Switch emulators
22
{
33
pkgs,
44
config,
55
lib,
66
...
77
}:
8-
98
let
10-
borg-wrapper = pkgs.writeScript "borg-wrapper" ''
11-
#!${lib.getExe pkgs.fish}
12-
13-
# Parse arguments using argparse
14-
function parse_args
15-
argparse 'p/path=' 'o/output=' 'm/max=' 'h/help' -- $argv
16-
or return 1
17-
18-
if set -ql _flag_help
19-
echo "Usage: borg-wrapper -p|--path PATH -o|--output REPO -m|--max MAX_BACKUPS -- COMMAND..."
20-
echo " -p, --path PATH Path to backup"
21-
echo " -o, --output REPO Path to store backups"
22-
echo " -m, --max MAX Maximum number of backups to keep"
23-
echo " -h, --help Show this help"
24-
exit 0
25-
end
26-
27-
# Check required arguments
28-
if not set -ql _flag_path
29-
echo "Error: --path is required" >&2
30-
return 1
31-
end
32-
if not set -ql _flag_output
33-
echo "Error: --output is required" >&2
34-
return 1
35-
end
36-
37-
# Set defaults
38-
set -g BACKUP_PATH $_flag_path
39-
set -g BORG_REPO $_flag_output
40-
41-
if set -ql _flag_max
42-
set -g MAX_BACKUPS $_flag_max
43-
else
44-
set -g MAX_BACKUPS 30
45-
end
46-
47-
# Everything remaining is the command to execute
48-
set -g CMD $argv
49-
50-
# Verify we have a command
51-
if test (count $CMD) -eq 0
52-
echo "Error: No command specified after --" >&2
53-
return 1
54-
end
55-
end
56-
57-
# Parse the arguments
58-
parse_args $argv
59-
or exit 1
60-
61-
# Initialize Borg repository
62-
mkdir -p "$BORG_REPO"
63-
if not ${pkgs.borgbackup}/bin/borg list "$BORG_REPO" &>/dev/null
64-
echo "Initializing new Borg repository at $BORG_REPO"
65-
${pkgs.borgbackup}/bin/borg init --encryption=none "$BORG_REPO"
66-
end
67-
68-
# Backup functions with error suppression
69-
function create_backup
70-
set -l tag $argv[1]
71-
set -l timestamp (date +%Y%m%d-%H%M%S)
72-
echo "Creating $tag backup: $timestamp"
73-
74-
# Push to parent directory, backup the basename only, then pop back
75-
pushd (dirname "$BACKUP_PATH") >/dev/null
76-
${pkgs.borgbackup}/bin/borg create --stats --compression zstd,15 \
77-
--files-cache=mtime,size \
78-
--lock-wait 5 \
79-
"$BORG_REPO::$tag-$timestamp" (basename "$BACKUP_PATH") || true
80-
popd >/dev/null
81-
end
82-
83-
function prune_backups
84-
echo "Pruning old backups"
85-
${pkgs.borgbackup}/bin/borg prune --keep-last "$MAX_BACKUPS" --stats "$BORG_REPO" || true
86-
end
87-
88-
# Initial backup
89-
create_backup "initial"
90-
prune_backups
91-
92-
# Start emulator in a subprocess group
93-
fish -c "
94-
function on_exit
95-
exit 0
96-
end
97-
98-
trap on_exit INT TERM
99-
exec $CMD
100-
" &
101-
set PID (jobs -lp | tail -n1)
102-
103-
# Cleanup function
104-
function cleanup
105-
# Send TERM to process group
106-
kill -TERM -$PID 2>/dev/null || true
107-
wait $PID 2>/dev/null || true
108-
create_backup "final"
109-
prune_backups
110-
end
111-
112-
function on_exit --on-signal INT --on-signal TERM
113-
cleanup
114-
end
115-
116-
# Debounced backup trigger
117-
set last_backup (date +%s)
118-
set backup_cooldown 30 # Minimum seconds between backups
119-
120-
# Watch loop with timeout
121-
while kill -0 $PID 2>/dev/null
122-
# Wait for changes with 5-second timeout
123-
if ${pkgs.inotify-tools}/bin/inotifywait \
124-
-r \
125-
-qq \
126-
-e close_write,delete,moved_to \
127-
-t 5 \
128-
"$BACKUP_PATH"
129-
130-
set current_time (date +%s)
131-
if test (math "$current_time - $last_backup") -ge $backup_cooldown
132-
create_backup "auto"
133-
prune_backups
134-
set last_backup $current_time
135-
else
136-
echo "Skipping backup:" + (math "$backup_cooldown - ($current_time - $last_backup)") + "s cooldown remaining"
137-
end
138-
end
139-
end
140-
141-
cleanup
142-
exit 0
9+
# Backup script that systemd services will call
10+
backupScript = pkgs.writeShellScript "switch-backup" ''
11+
set -euo pipefail
12+
13+
LABEL="$1"
14+
SAVE_PATH="$2"
15+
BACKUP_PATH="$3"
16+
17+
# Expand tilde if present
18+
SAVE_PATH="$(eval echo "$SAVE_PATH")"
19+
BACKUP_PATH="$(eval echo "$BACKUP_PATH")"
20+
21+
# Initialize Borg repo if needed
22+
mkdir -p "$BACKUP_PATH"
23+
if ! ${pkgs.borgbackup}/bin/borg list "$BACKUP_PATH" &>/dev/null; then
24+
${pkgs.borgbackup}/bin/borg init --encryption=none "$BACKUP_PATH"
25+
fi
26+
27+
# Create backup
28+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
29+
echo "[Backup] Creating $LABEL-$TIMESTAMP"
30+
31+
cd "$(dirname "$SAVE_PATH")"
32+
${pkgs.borgbackup}/bin/borg create \
33+
--compression zstd,15 \
34+
"$BACKUP_PATH::$LABEL-$TIMESTAMP" \
35+
"$(basename "$SAVE_PATH")" 2>&1 | grep "This archive" || true
36+
37+
# Prune old backups
38+
${pkgs.borgbackup}/bin/borg prune --keep-last 50 "$BACKUP_PATH" 2>/dev/null
14339
'';
14440

145-
# Generic function to create launcher scripts
146-
mkLaunchCommand =
147-
{
148-
savePath, # Path to the save directory
149-
backupPath, # Path where backups should be stored
150-
maxBackups ? 30, # Maximum number of backups to keep
151-
command, # Command to execute
152-
}:
153-
"${borg-wrapper} -p \"${savePath}\" -o \"${backupPath}\" -m ${toString maxBackups} -- ${command}";
41+
# Main emulator service
42+
mkEmulatorService = name: emulatorCmd: savePath: backupPath: {
43+
"emulator-${lib.toLower name}" = {
44+
Unit = {
45+
Description = "${name} emulator with automatic save backups";
46+
After = [ "graphical-session.target" ];
47+
# Ensure timer starts/stops with service
48+
Wants = [ "emulator-${lib.toLower name}-backup.timer" ];
49+
};
15450

155-
in
156-
{
157-
home.packages = with pkgs; [
158-
ryubing
159-
borgbackup
160-
inotify-tools
161-
];
51+
Service = {
52+
Type = "simple";
16253

163-
xdg.desktopEntries = {
164-
Ryujinx = {
165-
name = "Ryujinx w/ Borg Backups";
166-
comment = "Ryujinx Emulator with Borg Backups";
167-
exec = mkLaunchCommand {
168-
savePath = "~/.config/Ryujinx/bis/user/save";
169-
backupPath = "/pool/Backups/Switch/RyubingSaves";
170-
maxBackups = 30;
171-
command = "ryujinx";
54+
# Backup on start
55+
ExecStartPre = "${backupScript} start '${savePath}' '${backupPath}'";
56+
57+
# Run emulator
58+
ExecStart = emulatorCmd;
59+
60+
# Start timer for periodic backups
61+
ExecStartPost = "${pkgs.systemd}/bin/systemctl --user start emulator-${lib.toLower name}-backup.timer";
62+
63+
# Stop timer and backup on stop
64+
ExecStopPost = [
65+
"${pkgs.systemd}/bin/systemctl --user stop emulator-${lib.toLower name}-backup.timer"
66+
"${pkgs.bash}/bin/bash -c 'sleep 3; ${backupScript} stop \"${savePath}\" \"${backupPath}\"'"
67+
];
68+
69+
# Restart policy
70+
Restart = "on-failure";
71+
RestartSec = "5s";
72+
};
73+
74+
Install = {
75+
WantedBy = [ "default.target" ];
76+
};
77+
};
78+
};
79+
80+
# One-shot service for timer-triggered backups
81+
mkBackupService = name: savePath: backupPath: {
82+
"emulator-${lib.toLower name}-backup" = {
83+
Unit = {
84+
Description = "Backup ${name} saves";
85+
# Only run when emulator service is active
86+
Requisite = [ "emulator-${lib.toLower name}.service" ];
87+
};
88+
89+
Service = {
90+
Type = "oneshot";
91+
ExecStart = "${backupScript} interval '${savePath}' '${backupPath}'";
92+
};
93+
};
94+
};
95+
96+
# Timer for periodic backups
97+
mkBackupTimer = name: {
98+
"emulator-${lib.toLower name}-backup" = {
99+
Unit = {
100+
Description = "Periodic backup timer for ${name} saves";
101+
# Timer is controlled by the emulator service
102+
PartOf = [ "emulator-${lib.toLower name}.service" ];
103+
};
104+
105+
Timer = {
106+
OnActiveSec = "5min";
107+
OnUnitActiveSec = "5min";
108+
Unit = "emulator-${lib.toLower name}-backup.service";
172109
};
173-
icon = "Ryujinx";
110+
111+
# No Install section - timer is started/stopped by the emulator service
112+
};
113+
};
114+
115+
# Desktop entry that starts the systemd service
116+
mkEmulatorDesktopEntry =
117+
{
118+
name,
119+
icon ? "applications-games",
120+
wmClass ? name,
121+
...
122+
}@args:
123+
{
124+
name = "${name} (Protected)";
125+
comment = "${name} with automatic save backups";
126+
exec = "systemctl --user start emulator-${lib.toLower name}.service";
127+
icon = icon;
174128
type = "Application";
175129
terminal = false;
176130
categories = [
@@ -186,40 +140,54 @@ in
186140
];
187141
prefersNonDefaultGPU = true;
188142
settings = {
189-
StartupWMClass = "Ryujinx";
143+
StartupWMClass = wmClass;
190144
GenericName = "Nintendo Switch Emulator";
191-
};
145+
}
146+
// (args.settings or { });
192147
};
193148

194-
# FIXME: change to edenemu
195-
# citron-emu = {
196-
# name = "Citron w/ Borg Backups";
197-
# comment = "Citron Emulator with Borg Backups";
198-
# exec = mkLaunchCommand {
199-
# savePath = "${homeDir}/.local/share/citron/nand/user/save";
200-
# backupPath = "/pool/Backups/Switch/CitronSaves";
201-
# maxBackups = 30;
202-
# command = "citron-emu";
203-
# };
204-
# icon = "applications-games";
205-
# type = "Application";
206-
# terminal = false;
207-
# categories = [
208-
# "Game"
209-
# "Emulator"
210-
# ];
211-
# mimeType = [
212-
# "application/x-nx-nca"
213-
# "application/x-nx-nro"
214-
# "application/x-nx-nso"
215-
# "application/x-nx-nsp"
216-
# "application/x-nx-xci"
217-
# ];
218-
# prefersNonDefaultGPU = true;
219-
# settings = {
220-
# StartupWMClass = "Citron";
221-
# GenericName = "Nintendo Switch Emulator";
222-
# };
223-
# };
149+
gamescorperun = lib.getExe config.play.gamescoperun.package;
150+
151+
# Configuration for Ryubing emulator
152+
ryubingConfig = {
153+
name = "Ryubing";
154+
emulatorCmd = "${gamescorperun} ${lib.getExe pkgs.ryubing}";
155+
savePath = "~/.config/Ryujinx/bis/user/save";
156+
backupPath = "~/.switch/RyubingBackups";
157+
icon = "Ryujinx";
158+
wmClass = "Ryubing";
159+
};
160+
in
161+
{
162+
home.packages = with pkgs; [
163+
borgbackup
164+
nsz
165+
ryubing
166+
];
167+
168+
# Systemd user services
169+
systemd.user.services = lib.mkMerge [
170+
(mkEmulatorService ryubingConfig.name ryubingConfig.emulatorCmd ryubingConfig.savePath
171+
ryubingConfig.backupPath
172+
)
173+
(mkBackupService ryubingConfig.name ryubingConfig.savePath ryubingConfig.backupPath)
174+
];
175+
176+
# Systemd user timers
177+
systemd.user.timers = mkBackupTimer ryubingConfig.name;
178+
179+
# Desktop entries
180+
xdg.desktopEntries = {
181+
ryubing = mkEmulatorDesktopEntry ryubingConfig;
182+
};
183+
184+
# Service management aliases
185+
home.shellAliases = {
186+
# Start/stop emulator
187+
start-ryubing = "systemctl --user start emulator-ryubing.service";
188+
stop-ryubing = "systemctl --user stop emulator-ryubing.service";
189+
status-ryubing = "systemctl --user status emulator-ryubing.service emulator-ryubing-backup.timer";
190+
# View logs
191+
ryubing-logs = "journalctl --user -u emulator-ryubing.service -u emulator-ryubing-backup.service -n 50";
224192
};
225193
}

0 commit comments

Comments
 (0)