agent v0.3.8: fix crash UI notifications, auto Podman machine DNS, WordPress 7.0.0 ready template
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
VERSION="0.3.5"
|
||||
VERSION="0.3.8"
|
||||
APP_NAME="studioE5"
|
||||
BIN_NAME="studioE5-agent"
|
||||
LDFLAGS="-X main.version=${VERSION}"
|
||||
|
||||
@@ -39,6 +39,15 @@ func writeCompose(dataDir, instanceID, compose string, port int) error {
|
||||
return os.WriteFile(f, []byte(compose), 0644)
|
||||
}
|
||||
|
||||
func writeInitScript(dataDir, instanceID, script string) error {
|
||||
dir := instanceDir(dataDir, instanceID)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
f := filepath.Join(dir, "wp-init.sh")
|
||||
return os.WriteFile(f, []byte(script), 0755)
|
||||
}
|
||||
|
||||
func configureEngineCmd(cmd *exec.Cmd, dir string) {
|
||||
hideWindow(cmd)
|
||||
logPath := filepath.Join(dir, "compose.log")
|
||||
|
||||
+16
-2
@@ -62,6 +62,10 @@ func main() {
|
||||
|
||||
log.Printf("[%s Agent] version=%s node=%s data-dir=%s server=%s", APP_NAME, version, cfg.NodeID, *dataDir, cfg.Server)
|
||||
|
||||
// Ensure Podman machine DNS is configured on Windows/macOS so images can be
|
||||
// pulled and containers can reach the internet.
|
||||
ensurePodmanMachineDNS()
|
||||
|
||||
if *uiEnabled {
|
||||
go startUI(*dataDir, cfg.NodeID, cfg.Server)
|
||||
}
|
||||
@@ -84,6 +88,11 @@ func main() {
|
||||
cleanupWg.Add(1)
|
||||
go func() {
|
||||
defer cleanupWg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC in cleanup goroutine: %v", r)
|
||||
}
|
||||
}()
|
||||
<-shutdownCh
|
||||
log.Println("Cleaning up before exit...")
|
||||
|
||||
@@ -98,8 +107,8 @@ func main() {
|
||||
if info.Status == "running" {
|
||||
log.Printf("Stopping instance %s", id)
|
||||
_ = dockerComposeStop(*dataDir, id)
|
||||
info.Status = "stopped"
|
||||
_ = sendMessage(WSMessage{Action: "instance_stopped", InstanceID: id})
|
||||
inst[id].Status = "stopped"
|
||||
go sendMessage(WSMessage{Action: "instance_stopped", InstanceID: id})
|
||||
}
|
||||
}
|
||||
_ = saveInstances(*dataDir, inst)
|
||||
@@ -127,6 +136,11 @@ func main() {
|
||||
}
|
||||
|
||||
func startTailscaleAndReport(dataDir, nodeID, headscaleURL, authKey string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC in startTailscaleAndReport: %v", r)
|
||||
}
|
||||
}()
|
||||
ip, err := startTailscale(dataDir, nodeID, headscaleURL, authKey)
|
||||
if err != nil {
|
||||
log.Printf("Tailscale error: %v", err)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type podmanMachine struct {
|
||||
Name string `json:"name"`
|
||||
Running bool `json:"running"`
|
||||
VMType string `json:"vm_type"`
|
||||
}
|
||||
|
||||
// ensurePodmanMachineDNS configures public DNS resolvers on running Podman
|
||||
// machines on Windows and macOS. This is required because the Podman VM does
|
||||
// not always inherit a working DNS from the host, which prevents pulling
|
||||
// images and reaching api.wordpress.org from containers.
|
||||
func ensurePodmanMachineDNS() {
|
||||
if runtime.GOOS != "windows" && runtime.GOOS != "darwin" {
|
||||
return
|
||||
}
|
||||
if getContainerEngine() != "podman" {
|
||||
return
|
||||
}
|
||||
|
||||
out, err := exec.Command("podman", "machine", "list", "--format", "json").Output()
|
||||
if err != nil {
|
||||
log.Printf("ensurePodmanMachineDNS: cannot list machines: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var machines []podmanMachine
|
||||
if err := json.Unmarshal(out, &machines); err != nil {
|
||||
log.Printf("ensurePodmanMachineDNS: cannot parse machine list: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, m := range machines {
|
||||
if !m.Running {
|
||||
continue
|
||||
}
|
||||
if err := configurePodmanMachineDNS(m.Name); err != nil {
|
||||
log.Printf("ensurePodmanMachineDNS: failed for %s: %v", m.Name, err)
|
||||
} else {
|
||||
log.Printf("ensurePodmanMachineDNS: DNS configured for %s", m.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func configurePodmanMachineDNS(name string) error {
|
||||
cmd := exec.Command(
|
||||
"podman", "machine", "ssh", name,
|
||||
"sudo", "sh", "-c",
|
||||
"echo nameserver 8.8.8.8 > /etc/resolv.conf && echo nameserver 1.1.1.1 >> /etc/resolv.conf",
|
||||
)
|
||||
hideWindow(cmd)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+23
-9
@@ -251,9 +251,16 @@ func sendUILog(message string) {
|
||||
"level": "info",
|
||||
}
|
||||
for _, conn := range conns {
|
||||
if err := conn.WriteJSON(msg); err != nil {
|
||||
// Client may have disconnected; ignore.
|
||||
}
|
||||
func(c *websocket.Conn) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC in sendUILog: %v", r)
|
||||
}
|
||||
}()
|
||||
if err := c.WriteJSON(msg); err != nil {
|
||||
// Client may have disconnected; ignore.
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,9 +285,16 @@ func broadcastUI(msg map[string]interface{}) {
|
||||
uiConnectionsMu.RUnlock()
|
||||
|
||||
for _, conn := range conns {
|
||||
if err := conn.WriteJSON(msg); err != nil {
|
||||
// Ignore write errors for disconnected clients.
|
||||
}
|
||||
func(c *websocket.Conn) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC in broadcastUI: %v", r)
|
||||
}
|
||||
}()
|
||||
if err := c.WriteJSON(msg); err != nil {
|
||||
// Ignore write errors for disconnected clients.
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +430,7 @@ func uiStopInstance(dataDir, instanceID string) {
|
||||
inst[instanceID].Status = "stopped"
|
||||
_ = saveInstances(dataDir, inst)
|
||||
}
|
||||
_ = sendMessage(WSMessage{Action: "instance_stopped", InstanceID: instanceID})
|
||||
go sendMessage(WSMessage{Action: "instance_stopped", InstanceID: instanceID})
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
}
|
||||
|
||||
@@ -427,7 +441,7 @@ func uiDeleteInstance(dataDir, instanceID string) {
|
||||
}
|
||||
dockerComposeRm(dataDir, instanceID)
|
||||
removeInstance(dataDir, instanceID)
|
||||
_ = sendMessage(WSMessage{Action: "instance_deleted", InstanceID: instanceID})
|
||||
go sendMessage(WSMessage{Action: "instance_deleted", InstanceID: instanceID})
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
}
|
||||
|
||||
@@ -446,7 +460,7 @@ func uiResetInstance(dataDir, nodeID, instanceID string) {
|
||||
return
|
||||
}
|
||||
dockerComposeRm(dataDir, instanceID)
|
||||
handleStartInstance(dataDir, nodeID, instanceID, info.TemplateName, string(composeBytes), info.Port)
|
||||
handleStartInstance(dataDir, nodeID, instanceID, info.TemplateName, string(composeBytes), "", info.Port)
|
||||
}
|
||||
|
||||
// instanceContainersExist returns true if compose containers already exist for this instance.
|
||||
|
||||
+67
-8
@@ -18,6 +18,7 @@ type WSMessage struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
ComposeConfig string `json:"composeConfig,omitempty"`
|
||||
InitScript string `json:"initScript,omitempty"`
|
||||
StudentId string `json:"studentId,omitempty"`
|
||||
StudentName string `json:"studentName,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
@@ -101,7 +102,14 @@ func notifyUI(msg map[string]interface{}) {
|
||||
|
||||
log.Printf("notifyUI: broadcasting to %d UI clients", len(notifiers))
|
||||
for _, fn := range notifiers {
|
||||
go fn(msg)
|
||||
go func(notify uiNotifier) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC in notifyUI goroutine: %v", r)
|
||||
}
|
||||
}()
|
||||
notify(msg)
|
||||
}(fn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +200,12 @@ func startWebSocket(serverAddr, nodeID, dataDir, headscaleURL, headscaleAuthKey
|
||||
}
|
||||
|
||||
func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC in handleMessage (action=%s): %v", msg.Action, r)
|
||||
}
|
||||
}()
|
||||
|
||||
switch msg.Action {
|
||||
case "set_token":
|
||||
if msg.Token != "" {
|
||||
@@ -251,10 +265,15 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
hsURL, hsKey := getHeadscaleConfig()
|
||||
if hsURL == "" || hsKey == "" {
|
||||
log.Printf("Cannot start VPN: headscale config missing")
|
||||
sendMessage(WSMessage{Action: "vpn_error", NodeID: nodeID, Error: "headscale config missing"})
|
||||
go sendMessage(WSMessage{Action: "vpn_error", NodeID: nodeID, Error: "headscale config missing"})
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC in start_vpn goroutine: %v", r)
|
||||
}
|
||||
}()
|
||||
ip, err := startTailscale(dataDir, nodeID, hsURL, hsKey)
|
||||
if err != nil {
|
||||
log.Printf("start_vpn error: %v", err)
|
||||
@@ -282,7 +301,14 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
case "stop_vpn":
|
||||
log.Printf("Server requested VPN stop")
|
||||
stopTailscale()
|
||||
sendMessage(WSMessage{Action: "vpn_stopped", NodeID: nodeID})
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC in stop_vpn goroutine: %v", r)
|
||||
}
|
||||
}()
|
||||
sendMessage(WSMessage{Action: "vpn_stopped", NodeID: nodeID})
|
||||
}()
|
||||
case "activation_failed":
|
||||
log.Printf("handleMessage: activation_failed received, error=%s", msg.Error)
|
||||
notifyUI(map[string]interface{}{
|
||||
@@ -291,19 +317,27 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
})
|
||||
case "start":
|
||||
log.Printf("Start instance %s on port %d", msg.InstanceID, msg.Port)
|
||||
go handleStartInstance(dataDir, nodeID, msg.InstanceID, msg.Type, msg.ComposeConfig, msg.Port)
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC in start goroutine instance=%s: %v", msg.InstanceID, r)
|
||||
}
|
||||
}()
|
||||
handleStartInstance(dataDir, nodeID, msg.InstanceID, msg.Type, msg.ComposeConfig, msg.InitScript, msg.Port)
|
||||
}()
|
||||
case "stop":
|
||||
log.Printf("Stop instance %s", msg.InstanceID)
|
||||
if inst, _ := loadInstances(dataDir); inst[msg.InstanceID] != nil {
|
||||
removeTailscaleServe(inst[msg.InstanceID].Port)
|
||||
}
|
||||
if err := dockerComposeDown(dataDir, msg.InstanceID); err != nil {
|
||||
log.Printf("dockerComposeDown error: %v", err)
|
||||
if err := dockerComposeStop(dataDir, msg.InstanceID); err != nil {
|
||||
log.Printf("dockerComposeStop error: %v", err)
|
||||
}
|
||||
if inst, _ := loadInstances(dataDir); inst[msg.InstanceID] != nil {
|
||||
inst[msg.InstanceID].Status = "stopped"
|
||||
_ = saveInstances(dataDir, inst)
|
||||
}
|
||||
go sendMessage(WSMessage{Action: "instance_stopped", InstanceID: msg.InstanceID})
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
case "delete":
|
||||
log.Printf("Delete instance %s", msg.InstanceID)
|
||||
@@ -312,17 +346,37 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
}
|
||||
dockerComposeRm(dataDir, msg.InstanceID)
|
||||
removeInstance(dataDir, msg.InstanceID)
|
||||
go sendMessage(WSMessage{Action: "instance_deleted", InstanceID: msg.InstanceID})
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
case "reset":
|
||||
log.Printf("Reset instance %s", msg.InstanceID)
|
||||
dockerComposeRm(dataDir, msg.InstanceID)
|
||||
go handleStartInstance(dataDir, nodeID, msg.InstanceID, msg.Type, msg.ComposeConfig, msg.Port)
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC in reset goroutine instance=%s: %v", msg.InstanceID, r)
|
||||
}
|
||||
}()
|
||||
handleStartInstance(dataDir, nodeID, msg.InstanceID, msg.Type, msg.ComposeConfig, msg.InitScript, msg.Port)
|
||||
}()
|
||||
default:
|
||||
log.Printf("Unknown action: %s", msg.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func handleStartInstance(dataDir, nodeID, instanceID, instanceType, composeConfig string, port int) {
|
||||
func handleStartInstance(dataDir, nodeID, instanceID, instanceType, composeConfig, initScript string, port int) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC in handleStartInstance instance=%s: %v", instanceID, r)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: instanceID, TemplateName: instanceType, Port: port, Status: "error"})
|
||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: instanceID, Error: fmt.Sprintf("internal panic: %v", r)})
|
||||
sendInstanceProgress(instanceID, "start", "0", "Erreur interne")
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("handleStartInstance begin: instance=%s type=%s port=%d dataDir=%s initScriptLen=%d", instanceID, instanceType, port, dataDir, len(initScript))
|
||||
|
||||
notifyInstanceProgress := func(percent, message string) {
|
||||
sendInstanceProgress(instanceID, "start", percent, message)
|
||||
}
|
||||
@@ -344,6 +398,11 @@ func handleStartInstance(dataDir, nodeID, instanceID, instanceType, composeConfi
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
return
|
||||
}
|
||||
if initScript != "" {
|
||||
if err := writeInitScript(dataDir, instanceID, initScript); err != nil {
|
||||
log.Printf("writeInitScript error: %v", err)
|
||||
}
|
||||
}
|
||||
notifyInstanceProgress("30", "Configuration de l'application...")
|
||||
|
||||
if err := dockerComposeUp(dataDir, instanceID); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user