fix(agent/windows): named pipe Tailscale + hideWindow + logs
- Use Windows named pipe \.\pipe\studioe5-tailscaled instead of Unix socket - Apply hideWindow to all child processes (tailscale, podman, docker, browser) - Redirect agent logs to <data-dir>/agent.log and tailscaled logs to tailscaled.log - Fix double tailscale/ tailscale dir path in startTailscaleAndReport - Remove --operator=root on Windows - Bump agent version to 0.3.1
This commit is contained in:
+26
-3
@@ -88,6 +88,29 @@ Instance de test créée :
|
||||
- Template : `wordpress-wordpress-latest`
|
||||
- État : WordPress répond sur `http://127.0.0.1:8001` **et en HTTPS public sur `https://test-wp-001.studioe5.edudeploy.com/`**.
|
||||
|
||||
## 🪟 Fix agent Windows v0.3.1
|
||||
|
||||
Problème rencontré sur le PC de test (`OMEGA-GAMER-dc166b1a`) :
|
||||
- Le nœud apparaissait `online` dans le dashboard mais sans IP Tailscale.
|
||||
- `tailscale.exe ip -4` retournait une erreur de connexion au socket local.
|
||||
|
||||
Cause racine :
|
||||
- L’agent lançait `tailscaled` avec `--socket=<fichier>.sock`, mais **Tailscale sur Windows utilise des named pipes** (`\\.\pipe\...`), pas des sockets Unix.
|
||||
- De plus, les commandes `podman`/`docker`/`tailscale` ouvraient une fenêtre console à chaque exécution.
|
||||
|
||||
Corrections apportées (`agent/tailscale.go`, `agent/docker.go`, `agent/instance.go`, `agent/systray.go`, `agent/ui.go`, `agent/main.go`) :
|
||||
- Sur Windows, utilisation de la named pipe `\\.\pipe\studioe5-tailscaled`.
|
||||
- Application de `hideWindow` à tous les processus enfants (Tailscale, Podman, Docker, ouverture navigateur, redémarrage agent).
|
||||
- Redirection des logs agent vers `<data-dir>/agent.log` et des logs `tailscaled` vers `<data-dir>/tailscale/tailscaled.log`.
|
||||
- Suppression de `--operator=root` sur Windows (non pertinent).
|
||||
- Correction du chemin `dataDir` passé à `startTailscale` (évitait un double dossier `tailscale/tailscale`).
|
||||
|
||||
Validation manuelle sur Windows :
|
||||
```powershell
|
||||
.\tailscaled.exe --state="C:\...\data\tailscale.state" --socket="\\.\pipe\studioe5-tailscaled" --tun=userspace-networking
|
||||
.\tailscale.exe --socket="\\.\pipe\studioe5-tailscaled" status # => Logged out (NeedsLogin)
|
||||
```
|
||||
|
||||
## 🛠️ Commandes utiles pour reprendre
|
||||
|
||||
### Voir l’agent de test
|
||||
@@ -166,11 +189,11 @@ L’agent est servi par Caddy depuis le dossier `agent/` monté dans le conteneu
|
||||
|
||||
### Binaires disponibles
|
||||
|
||||
- **Windows (archive complète)** : `https://studioe5.edudeploy.com/studioE5-agent-v0.3.0-windows.zip`
|
||||
- **Windows (archive complète)** : `https://studioe5.edudeploy.com/studioE5-agent-v0.3.1-windows.zip`
|
||||
- Contient `studioE5-agent.exe` + `tailscale-bin/windows/` (`tailscale.exe`, `tailscaled.exe`, `wintun.dll`) + `README-Windows.txt`.
|
||||
- **Windows (exécutable seul)** : `https://studioe5.edudeploy.com/studioE5-agent-v0.3.0.exe`
|
||||
- **Windows (exécutable seul)** : `https://studioe5.edudeploy.com/studioE5-agent-v0.3.1.exe`
|
||||
- Nécessite d’avoir installé Tailscale Windows séparément ou d’avoir les binaires dans `tailscale-bin/windows/`.
|
||||
- **Linux** : `https://studioe5.edudeploy.com/studioE5-agent-v0.3.0`
|
||||
- **Linux** : `https://studioe5.edudeploy.com/studioE5-agent-v0.3.1`
|
||||
|
||||
### Builder / préparer les binaires
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
VERSION="0.3.0"
|
||||
VERSION="0.3.1"
|
||||
APP_NAME="studioE5"
|
||||
BIN_NAME="studioE5-agent"
|
||||
LDFLAGS="-X main.version=${VERSION}"
|
||||
|
||||
+16
-14
@@ -39,27 +39,33 @@ func writeCompose(dataDir, instanceID, compose string, port int) error {
|
||||
return os.WriteFile(f, []byte(compose), 0644)
|
||||
}
|
||||
|
||||
func configureEngineCmd(cmd *exec.Cmd, dir string) {
|
||||
hideWindow(cmd)
|
||||
logPath := filepath.Join(dir, "compose.log")
|
||||
if f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
|
||||
cmd.Stdout = f
|
||||
cmd.Stderr = f
|
||||
}
|
||||
}
|
||||
|
||||
func dockerComposeUp(dataDir, instanceID string) error {
|
||||
dir := instanceDir(dataDir, instanceID)
|
||||
cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "up", "-d")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
configureEngineCmd(cmd, dir)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func dockerComposeDown(dataDir, instanceID string) error {
|
||||
dir := instanceDir(dataDir, instanceID)
|
||||
cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
configureEngineCmd(cmd, dir)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func dockerComposeRm(dataDir, instanceID string) error {
|
||||
dir := instanceDir(dataDir, instanceID)
|
||||
cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down", "-v")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
configureEngineCmd(cmd, dir)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -104,15 +110,13 @@ fi
|
||||
defer os.Remove(scriptPath)
|
||||
|
||||
cpCmd := exec.Command(engine, "compose", "-f", composeFile, "cp", scriptPath, "app:/tmp/update-wp-urls.sh")
|
||||
cpCmd.Stdout = os.Stdout
|
||||
cpCmd.Stderr = os.Stderr
|
||||
configureEngineCmd(cpCmd, dir)
|
||||
if err := cpCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
execCmd := exec.Command(engine, "compose", "-f", composeFile, "exec", "-T", "app", "sh", "/tmp/update-wp-urls.sh")
|
||||
execCmd.Stdout = os.Stdout
|
||||
execCmd.Stderr = os.Stderr
|
||||
configureEngineCmd(execCmd, dir)
|
||||
return execCmd.Run()
|
||||
}
|
||||
|
||||
@@ -141,14 +145,12 @@ fi
|
||||
defer os.Remove(scriptPath)
|
||||
|
||||
cpCmd := exec.Command(engine, "compose", "-f", composeFile, "cp", scriptPath, "app:/tmp/strip-wp-urls.sh")
|
||||
cpCmd.Stdout = os.Stdout
|
||||
cpCmd.Stderr = os.Stderr
|
||||
configureEngineCmd(cpCmd, dir)
|
||||
if err := cpCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
execCmd := exec.Command(engine, "compose", "-f", composeFile, "exec", "-T", "app", "sh", "/tmp/strip-wp-urls.sh")
|
||||
execCmd.Stdout = os.Stdout
|
||||
execCmd.Stderr = os.Stderr
|
||||
configureEngineCmd(execCmd, dir)
|
||||
return execCmd.Run()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
import "os/exec"
|
||||
|
||||
// hideWindow is a no-op on non-Windows platforms.
|
||||
func hideWindow(cmd *exec.Cmd) {}
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// hideWindow configures a command so that it does not open a console window
|
||||
// when it starts. This is essential for the agent running on student Windows
|
||||
// machines, otherwise every docker/podman/tailscale command flashes a window.
|
||||
func hideWindow(cmd *exec.Cmd) {
|
||||
if cmd.SysProcAttr == nil {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
}
|
||||
cmd.SysProcAttr.HideWindow = true
|
||||
}
|
||||
@@ -88,6 +88,7 @@ func getInstanceStatus(dataDir, instanceID string) string {
|
||||
|
||||
// Try modern JSON format first
|
||||
cmd := exec.Command(engine, "compose", "-f", composeFile, "ps", "--format", "json")
|
||||
hideWindow(cmd)
|
||||
out, err := cmd.Output()
|
||||
if err == nil {
|
||||
outStr := strings.TrimSpace(string(out))
|
||||
@@ -119,6 +120,7 @@ func getInstanceStatus(dataDir, instanceID string) string {
|
||||
|
||||
// Fallback: use "ps -q" which is supported by all docker-compose versions
|
||||
cmd = exec.Command(engine, "compose", "-f", composeFile, "ps", "-q")
|
||||
hideWindow(cmd)
|
||||
out, err = cmd.Output()
|
||||
if err != nil {
|
||||
return "error"
|
||||
|
||||
+9
-2
@@ -42,6 +42,14 @@ func main() {
|
||||
log.Fatalf("Cannot create data-dir: %v", err)
|
||||
}
|
||||
|
||||
// Redirect agent logs to a file so the console can be hidden on Windows.
|
||||
agentLogPath := filepath.Join(*dataDir, "agent.log")
|
||||
if agentLogFile, err := os.OpenFile(agentLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
|
||||
log.SetOutput(agentLogFile)
|
||||
} else {
|
||||
log.Printf("Cannot open agent log file %s: %v", agentLogPath, err)
|
||||
}
|
||||
|
||||
cfg, _, err := loadOrCreateConfig(*dataDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot load config: %v", err)
|
||||
@@ -78,8 +86,7 @@ func main() {
|
||||
}
|
||||
|
||||
func startTailscaleAndReport(dataDir, nodeID, headscaleURL, authKey string) {
|
||||
tsDir := filepath.Join(dataDir, "tailscale")
|
||||
ip, err := startTailscale(tsDir, nodeID, headscaleURL, authKey)
|
||||
ip, err := startTailscale(dataDir, nodeID, headscaleURL, authKey)
|
||||
if err != nil {
|
||||
log.Printf("Tailscale error: %v", err)
|
||||
return
|
||||
|
||||
+3
-1
@@ -80,7 +80,9 @@ func openBrowser(url string) {
|
||||
args = []string{url}
|
||||
}
|
||||
|
||||
if err := exec.Command(cmd, args...).Start(); err != nil {
|
||||
openCmd := exec.Command(cmd, args...)
|
||||
hideWindow(openCmd)
|
||||
if err := openCmd.Start(); err != nil {
|
||||
log.Printf("Failed to open browser: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
+32
-11
@@ -61,17 +61,29 @@ func startTailscale(dataDir, nodeID, headscaleURL, authKey string) (string, erro
|
||||
if err := os.MkdirAll(tsDataDir, 0700); err != nil {
|
||||
return "", fmt.Errorf("create tailscale dir: %w", err)
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows uses named pipes for tailscaled IPC, not Unix sockets.
|
||||
tsSocket = `\\.\pipe\studioe5-tailscaled`
|
||||
} else {
|
||||
tsSocket = filepath.Join(tsDataDir, "tailscaled.sock")
|
||||
}
|
||||
stateFile := filepath.Join(tsDataDir, "tailscaled.state")
|
||||
|
||||
log.Printf("Starting tailscaled for node %s", nodeID)
|
||||
log.Printf("Starting tailscaled for node %s (socket=%s)", nodeID, tsSocket)
|
||||
tsCmd = exec.Command(tailscaleBin("tailscaled"),
|
||||
"--state="+stateFile,
|
||||
"--socket="+tsSocket,
|
||||
"--tun=userspace-networking",
|
||||
)
|
||||
tsCmd.Stdout = os.Stdout
|
||||
tsCmd.Stderr = os.Stderr
|
||||
hideWindow(tsCmd)
|
||||
// Redirect tailscaled output to a dedicated log file.
|
||||
tsLogPath := filepath.Join(tsDataDir, "tailscaled.log")
|
||||
if tsLogFile, err := os.OpenFile(tsLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
|
||||
tsCmd.Stdout = tsLogFile
|
||||
tsCmd.Stderr = tsLogFile
|
||||
} else {
|
||||
log.Printf("Cannot open tailscaled log file %s: %v", tsLogPath, err)
|
||||
}
|
||||
if err := tsCmd.Start(); err != nil {
|
||||
tsCmd = nil
|
||||
return "", fmt.Errorf("start tailscaled: %w", err)
|
||||
@@ -84,17 +96,22 @@ func startTailscale(dataDir, nodeID, headscaleURL, authKey string) (string, erro
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
upCmd := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
||||
upArgs := []string{
|
||||
"--socket=" + tsSocket,
|
||||
"up",
|
||||
"--authkey=" + authKey,
|
||||
"--login-server=" + headscaleURL,
|
||||
"--hostname=" + nodeID,
|
||||
"--accept-dns=false",
|
||||
"--operator=root",
|
||||
)
|
||||
upCmd.Stdout = os.Stdout
|
||||
upCmd.Stderr = os.Stderr
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
// --operator is only meaningful on Unix systems.
|
||||
upArgs = append(upArgs, "--operator=root")
|
||||
}
|
||||
upCmd := exec.CommandContext(ctx, tailscaleBin("tailscale"), upArgs...)
|
||||
hideWindow(upCmd)
|
||||
upCmd.Stdout = log.Writer()
|
||||
upCmd.Stderr = log.Writer()
|
||||
if err := upCmd.Run(); err != nil {
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
@@ -104,10 +121,12 @@ func startTailscale(dataDir, nodeID, headscaleURL, authKey string) (string, erro
|
||||
|
||||
// Wait for an IP address.
|
||||
for {
|
||||
out, err := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
||||
statusCmd := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
||||
"--socket="+tsSocket,
|
||||
"status", "--json",
|
||||
).Output()
|
||||
)
|
||||
hideWindow(statusCmd)
|
||||
out, err := statusCmd.Output()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -151,7 +170,9 @@ func stopTailscaleLocked() {
|
||||
return
|
||||
}
|
||||
if tsSocket != "" {
|
||||
_ = exec.Command(tailscaleBin("tailscale"), "--socket="+tsSocket, "down").Run()
|
||||
downCmd := exec.Command(tailscaleBin("tailscale"), "--socket="+tsSocket, "down")
|
||||
hideWindow(downCmd)
|
||||
_ = downCmd.Run()
|
||||
}
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
|
||||
@@ -61,6 +61,7 @@ func startUI(dataDir, nodeID, serverAddr string) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
go func() {
|
||||
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||
hideWindow(cmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
Reference in New Issue
Block a user