174 lines
3.6 KiB
Go
174 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var preservePaths = map[string]bool{
|
|
"config/settings.json": true,
|
|
"data": true,
|
|
}
|
|
|
|
func shouldPreserve(rel string) bool {
|
|
rel = filepath.ToSlash(rel)
|
|
for p := range preservePaths {
|
|
if rel == p || strings.HasPrefix(rel, p+"/") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isProcessRunning(pid int) bool {
|
|
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid), "/NH")
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return strings.Contains(string(out), fmt.Sprintf("%d", pid))
|
|
}
|
|
|
|
func main() {
|
|
pid := flag.Int("pid", 0, "PID of main app to wait for")
|
|
zipPath := flag.String("zip", "", "Path to update zip")
|
|
target := flag.String("target", "", "Install directory")
|
|
launch := flag.String("launch", "", "Executable to launch after update")
|
|
flag.Parse()
|
|
|
|
if *zipPath == "" || *target == "" {
|
|
fmt.Println("Usage: updater --pid PID --zip FILE --target DIR --launch EXE")
|
|
os.Exit(1)
|
|
}
|
|
|
|
logsDir := filepath.Join(*target, "logs")
|
|
os.MkdirAll(logsDir, 0755)
|
|
logFile, _ := os.Create(filepath.Join(logsDir, "updater.log"))
|
|
log := func(msg string, args ...interface{}) {
|
|
line := fmt.Sprintf("[%s] %s\n", time.Now().Format("15:04:05"), fmt.Sprintf(msg, args...))
|
|
fmt.Print(line)
|
|
if logFile != nil {
|
|
logFile.WriteString(line)
|
|
}
|
|
}
|
|
|
|
if *pid > 0 {
|
|
log("Waiting for process %d to exit...", *pid)
|
|
for i := 0; i < 30; i++ {
|
|
if !isProcessRunning(*pid) {
|
|
log("Process exited")
|
|
break
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
log("Extracting %s", *zipPath)
|
|
tmpDir := *zipPath + "_extract"
|
|
os.RemoveAll(tmpDir)
|
|
if err := extractZip(*zipPath, tmpDir); err != nil {
|
|
log("Extract failed: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Auto-detect nested top-level directory and strip it
|
|
entries, _ := os.ReadDir(tmpDir)
|
|
if len(entries) == 1 && entries[0].IsDir() {
|
|
nested := filepath.Join(tmpDir, entries[0].Name())
|
|
log("Detected nested directory: %s, stripping", entries[0].Name())
|
|
tmpDir = nested
|
|
}
|
|
|
|
log("Installing to %s", *target)
|
|
count := 0
|
|
skipped := 0
|
|
err := filepath.Walk(tmpDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() {
|
|
return err
|
|
}
|
|
rel, _ := filepath.Rel(tmpDir, path)
|
|
if shouldPreserve(rel) {
|
|
log(" Skip: %s", rel)
|
|
skipped++
|
|
return nil
|
|
}
|
|
dst := filepath.Join(*target, rel)
|
|
os.MkdirAll(filepath.Dir(dst), 0755)
|
|
|
|
src, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
|
|
out, err := os.Create(dst)
|
|
if err != nil {
|
|
log(" Failed to write: %s (%v)", rel, err)
|
|
return nil
|
|
}
|
|
defer out.Close()
|
|
io.Copy(out, src)
|
|
count++
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log("Install error: %v", err)
|
|
}
|
|
log("Updated %d files, skipped %d", count, skipped)
|
|
|
|
os.RemoveAll(tmpDir)
|
|
os.Remove(*zipPath)
|
|
log("Cleanup done")
|
|
|
|
if *launch != "" {
|
|
exe := filepath.Join(*target, *launch)
|
|
log("Launching %s", exe)
|
|
cmd := exec.Command(exe)
|
|
cmd.Dir = *target
|
|
cmd.Start()
|
|
}
|
|
|
|
log("Update complete")
|
|
if logFile != nil {
|
|
logFile.Close()
|
|
}
|
|
}
|
|
|
|
func extractZip(src, dst string) error {
|
|
r, err := zip.OpenReader(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
|
|
for _, f := range r.File {
|
|
path := filepath.Join(dst, f.Name)
|
|
if f.FileInfo().IsDir() {
|
|
os.MkdirAll(path, 0755)
|
|
continue
|
|
}
|
|
os.MkdirAll(filepath.Dir(path), 0755)
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := os.Create(path)
|
|
if err != nil {
|
|
rc.Close()
|
|
return err
|
|
}
|
|
io.Copy(out, rc)
|
|
out.Close()
|
|
rc.Close()
|
|
}
|
|
return nil
|
|
}
|