fix(builder): don't count Docker's init or the Steam client's own files as lost saves
Every container the node-agent starts gets /usr/sbin/docker-init, and SteamCMD rewrites its own config.vdf on every run. Counted as files written outside the volumes, they made every build look like it loses saves. A folder whose only changes were logs no longer counts either, and a folder followed by a same-named file (worlds, worlds.bak) is no longer taken for a file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87dfca1285
commit
0a80a6ebc5
4 files changed
+71
-43
No files matched your search
@@ -30,7 +30,9 @@ func TestOutsideDirsSkipsSteamNoise(t *testing.T) {
|
||||
r := &bootReport{ContainerChanges: []string{
|
||||
"A /home/steam/.config/example-game/worlds/w.db",
|
||||
"A /home/steam/Steam/logs/connection_log.txt",
|
||||
"C /home/steam/Steam/config/config.vdf",
|
||||
"C /home/steam/.steam/registry.vdf",
|
||||
"A /usr/sbin/docker-init",
|
||||
}}
|
||||
got := outsideDirs(r)
|
||||
if len(got) != 1 || got[0] != "/home/steam/.config/example-game/worlds" {
|
||||
|
||||
+2
-11
@@ -336,9 +336,6 @@ func closestVar(name string, vars []string) string {
|
||||
return best
|
||||
}
|
||||
|
||||
// Files the Steam client, Proton and the OS write for themselves: not the game's data
|
||||
var outsideNoise = []string{"/Steam/logs/", "/.steam/", "/steamcmd/", "/.cache/", "/tmp/", "/.local/share/umu"}
|
||||
|
||||
// savesOutsideVolumes turns first boot's "written outside the volumes" into a hard check: anything the
|
||||
// game itself wrote there (worlds or player lists under the user's home folder) is lost every time the
|
||||
// node recreates the container. A template passes once its startup command or settings point the game at
|
||||
@@ -372,14 +369,8 @@ func outsideDirs(report *bootReport) []string {
|
||||
}
|
||||
dirs := map[string]bool{}
|
||||
for _, c := range report.ContainerChanges {
|
||||
_, p, _ := strings.Cut(c, " ")
|
||||
noise := false
|
||||
for _, n := range outsideNoise {
|
||||
if strings.Contains(p, n) {
|
||||
noise = true
|
||||
}
|
||||
}
|
||||
if !noise && p != "" {
|
||||
// A report saved by an older version can still list files that aren't the game's state
|
||||
if _, p, _ := strings.Cut(c, " "); p != "" && !notGameState(p) {
|
||||
dirs[path.Dir(p)] = true
|
||||
}
|
||||
}
|
||||
|
||||
+51
-31
@@ -512,16 +512,36 @@ func (b *booter) fileLines(report *bootReport) []logLine {
|
||||
return out
|
||||
}
|
||||
|
||||
// System folders and files that aren't the game's state: what's written there doesn't matter when lost
|
||||
// Files that aren't the game's state, so losing them doesn't matter: system folders, Docker's own init
|
||||
// (in every container started with --init), logs, lock and pid files, and what the Steam client, Proton
|
||||
// and the OS keep for themselves in the home folder
|
||||
var (
|
||||
systemPrefixes = []string{"/tmp", "/proc", "/sys", "/dev", "/run", "/etc", "/var/log", "/var/cache", "/var/tmp", "/var/lib/apt"}
|
||||
systemPrefixes = []string{"/tmp", "/proc", "/sys", "/dev", "/run", "/etc", "/var/log", "/var/cache", "/var/tmp", "/var/lib/apt", "/usr/sbin/docker-init"}
|
||||
transientExt = map[string]bool{".log": true, ".pid": true, ".lock": true, ".tmp": true, ".swp": true}
|
||||
clientNoise = []string{"/Steam/logs/", "/Steam/config/", "/Steam/appcache/", "/.steam/", "/steamcmd/", "/.cache/", "/tmp/", "/.local/share/umu"}
|
||||
)
|
||||
|
||||
func notGameState(p string) bool {
|
||||
if transientExt[strings.ToLower(path.Ext(p))] {
|
||||
return true
|
||||
}
|
||||
for _, prefix := range systemPrefixes {
|
||||
if p == prefix || strings.HasPrefix(p, prefix+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, n := range clientNoise {
|
||||
if strings.Contains(p, n) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// containerChanges lists the files the server wrote anywhere but /server and /data. All of them are
|
||||
// lost when the node recreates the container (every start): in the container's own filesystem, and in
|
||||
// the image's VOLUME folders, which Docker gives fresh anonymous volumes each time. Logs, lock and pid
|
||||
// files are left out; saves of any kind are not.
|
||||
// the image's VOLUME folders, which Docker gives fresh anonymous volumes each time. Files that aren't
|
||||
// the game's state are left out; saves of any kind are not.
|
||||
func (b *booter) containerChanges() []string {
|
||||
var changes []string
|
||||
if out, err := exec.Command("docker", "diff", b.container).Output(); err == nil {
|
||||
@@ -541,7 +561,7 @@ func (b *booter) containerChanges() []string {
|
||||
n := 0
|
||||
for _, f := range strings.Split(strings.TrimSpace(string(files)), "\n") {
|
||||
rel := strings.TrimPrefix(f, "/v/")
|
||||
if f == "" || transientExt[strings.ToLower(path.Ext(rel))] {
|
||||
if f == "" || notGameState(path.Join(dest, rel)) {
|
||||
continue
|
||||
}
|
||||
if n++; n > 200 {
|
||||
@@ -553,42 +573,42 @@ func (b *booter) containerChanges() []string {
|
||||
return changes
|
||||
}
|
||||
|
||||
// outsideFiles picks the files out of `docker diff` output: added or changed leaves (docker diff lists
|
||||
// folders too) outside the mounted volumes and system folders, without transient files.
|
||||
// outsideFiles picks the game's files out of `docker diff` output: added or changed outside the mounted
|
||||
// volumes. docker diff lists folders too, as changed when anything inside them changed, so a folder only
|
||||
// counts when nothing inside it did (it was created empty).
|
||||
func outsideFiles(diff string, mounts map[string]string) []string {
|
||||
var paths []string
|
||||
kinds := map[string]string{}
|
||||
parents := map[string]bool{}
|
||||
for _, line := range strings.Split(strings.TrimSpace(diff), "\n") {
|
||||
kind, p, ok := strings.Cut(strings.TrimSpace(line), " ")
|
||||
if !ok || kind == "D" || p == "" {
|
||||
if !ok || p == "" {
|
||||
continue
|
||||
}
|
||||
skip := transientExt[strings.ToLower(path.Ext(p))]
|
||||
for _, prefix := range systemPrefixes {
|
||||
if p == prefix || strings.HasPrefix(p, prefix+"/") {
|
||||
skip = true
|
||||
}
|
||||
}
|
||||
for mount := range mounts {
|
||||
if p == mount || strings.HasPrefix(p, mount+"/") {
|
||||
skip = true
|
||||
}
|
||||
}
|
||||
if !skip {
|
||||
paths = append(paths, p)
|
||||
kinds[p] = kind
|
||||
kinds[p] = kind
|
||||
for d := path.Dir(p); d != "/" && d != "."; d = path.Dir(d) {
|
||||
parents[d] = true
|
||||
}
|
||||
}
|
||||
sort.Strings(paths)
|
||||
var leaves []string
|
||||
for i, p := range paths {
|
||||
// A folder is followed (sorted) by the entries inside it
|
||||
if i+1 < len(paths) && strings.HasPrefix(paths[i+1], p+"/") {
|
||||
continue
|
||||
var files []string
|
||||
for p, kind := range kinds {
|
||||
if kind != "D" && !parents[p] && !notGameState(p) && !inMounts(p, mounts) {
|
||||
files = append(files, p)
|
||||
}
|
||||
leaves = append(leaves, kinds[p]+" "+p)
|
||||
}
|
||||
return leaves
|
||||
sort.Strings(files)
|
||||
for i, p := range files {
|
||||
files[i] = kinds[p] + " " + p
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func inMounts(p string, mounts map[string]string) bool {
|
||||
for mount := range mounts {
|
||||
if p == mount || strings.HasPrefix(p, mount+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *booter) cleanup() {
|
||||
|
||||
@@ -113,15 +113,30 @@ A /home/steam/.config/example
|
||||
A /home/steam/.config/example/worlds
|
||||
A /home/steam/.config/example/worlds/world1.db
|
||||
A /home/steam/.config/example/worlds/world1.fwl
|
||||
A /home/steam/.config/example/worlds.bak
|
||||
A /home/steam/.config/example/Player.log
|
||||
C /home/steam/logs
|
||||
A /home/steam/logs/server.log
|
||||
C /home/steam/Steam
|
||||
C /home/steam/Steam/config
|
||||
C /home/steam/Steam/config/config.vdf
|
||||
C /home/steam/steamcmd/linux64/steamclient.so
|
||||
C /etc
|
||||
C /etc/hostname
|
||||
C /usr
|
||||
C /usr/sbin
|
||||
A /usr/sbin/docker-init
|
||||
C /var
|
||||
C /var/log
|
||||
A /var/log/x
|
||||
A /tmp/x.sock
|
||||
A /server/should-not-count.sav
|
||||
D /home/steam/old.txt
|
||||
A /home/steam/server.pid`
|
||||
got := outsideFiles(diff, map[string]string{"/server": "v", "/data": "v"})
|
||||
want := []string{"A /home/steam/.config/example/worlds/world1.db", "A /home/steam/.config/example/worlds/world1.fwl"}
|
||||
// Folders whose only changes aren't the game's (logs, Docker's init) don't count either
|
||||
want := []string{"A /home/steam/.config/example/worlds.bak", "A /home/steam/.config/example/worlds/world1.db",
|
||||
"A /home/steam/.config/example/worlds/world1.fwl"}
|
||||
if strings.Join(got, "|") != strings.Join(want, "|") {
|
||||
t.Errorf("got %q\nwant %q", got, want)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user