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`
|
- 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/`**.
|
- É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
|
## 🛠️ Commandes utiles pour reprendre
|
||||||
|
|
||||||
### Voir l’agent de test
|
### 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
|
### 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`.
|
- 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/`.
|
- 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
|
### Builder / préparer les binaires
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VERSION="0.3.0"
|
VERSION="0.3.1"
|
||||||
APP_NAME="studioE5"
|
APP_NAME="studioE5"
|
||||||
BIN_NAME="studioE5-agent"
|
BIN_NAME="studioE5-agent"
|
||||||
LDFLAGS="-X main.version=${VERSION}"
|
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)
|
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 {
|
func dockerComposeUp(dataDir, instanceID string) error {
|
||||||
dir := instanceDir(dataDir, instanceID)
|
dir := instanceDir(dataDir, instanceID)
|
||||||
cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "up", "-d")
|
cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "up", "-d")
|
||||||
cmd.Stdout = os.Stdout
|
configureEngineCmd(cmd, dir)
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func dockerComposeDown(dataDir, instanceID string) error {
|
func dockerComposeDown(dataDir, instanceID string) error {
|
||||||
dir := instanceDir(dataDir, instanceID)
|
dir := instanceDir(dataDir, instanceID)
|
||||||
cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down")
|
cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down")
|
||||||
cmd.Stdout = os.Stdout
|
configureEngineCmd(cmd, dir)
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func dockerComposeRm(dataDir, instanceID string) error {
|
func dockerComposeRm(dataDir, instanceID string) error {
|
||||||
dir := instanceDir(dataDir, instanceID)
|
dir := instanceDir(dataDir, instanceID)
|
||||||
cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down", "-v")
|
cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down", "-v")
|
||||||
cmd.Stdout = os.Stdout
|
configureEngineCmd(cmd, dir)
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -104,15 +110,13 @@ fi
|
|||||||
defer os.Remove(scriptPath)
|
defer os.Remove(scriptPath)
|
||||||
|
|
||||||
cpCmd := exec.Command(engine, "compose", "-f", composeFile, "cp", scriptPath, "app:/tmp/update-wp-urls.sh")
|
cpCmd := exec.Command(engine, "compose", "-f", composeFile, "cp", scriptPath, "app:/tmp/update-wp-urls.sh")
|
||||||
cpCmd.Stdout = os.Stdout
|
configureEngineCmd(cpCmd, dir)
|
||||||
cpCmd.Stderr = os.Stderr
|
|
||||||
if err := cpCmd.Run(); err != nil {
|
if err := cpCmd.Run(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
execCmd := exec.Command(engine, "compose", "-f", composeFile, "exec", "-T", "app", "sh", "/tmp/update-wp-urls.sh")
|
execCmd := exec.Command(engine, "compose", "-f", composeFile, "exec", "-T", "app", "sh", "/tmp/update-wp-urls.sh")
|
||||||
execCmd.Stdout = os.Stdout
|
configureEngineCmd(execCmd, dir)
|
||||||
execCmd.Stderr = os.Stderr
|
|
||||||
return execCmd.Run()
|
return execCmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,14 +145,12 @@ fi
|
|||||||
defer os.Remove(scriptPath)
|
defer os.Remove(scriptPath)
|
||||||
|
|
||||||
cpCmd := exec.Command(engine, "compose", "-f", composeFile, "cp", scriptPath, "app:/tmp/strip-wp-urls.sh")
|
cpCmd := exec.Command(engine, "compose", "-f", composeFile, "cp", scriptPath, "app:/tmp/strip-wp-urls.sh")
|
||||||
cpCmd.Stdout = os.Stdout
|
configureEngineCmd(cpCmd, dir)
|
||||||
cpCmd.Stderr = os.Stderr
|
|
||||||
if err := cpCmd.Run(); err != nil {
|
if err := cpCmd.Run(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
execCmd := exec.Command(engine, "compose", "-f", composeFile, "exec", "-T", "app", "sh", "/tmp/strip-wp-urls.sh")
|
execCmd := exec.Command(engine, "compose", "-f", composeFile, "exec", "-T", "app", "sh", "/tmp/strip-wp-urls.sh")
|
||||||
execCmd.Stdout = os.Stdout
|
configureEngineCmd(execCmd, dir)
|
||||||
execCmd.Stderr = os.Stderr
|
|
||||||
return execCmd.Run()
|
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
|
// Try modern JSON format first
|
||||||
cmd := exec.Command(engine, "compose", "-f", composeFile, "ps", "--format", "json")
|
cmd := exec.Command(engine, "compose", "-f", composeFile, "ps", "--format", "json")
|
||||||
|
hideWindow(cmd)
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
outStr := strings.TrimSpace(string(out))
|
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
|
// Fallback: use "ps -q" which is supported by all docker-compose versions
|
||||||
cmd = exec.Command(engine, "compose", "-f", composeFile, "ps", "-q")
|
cmd = exec.Command(engine, "compose", "-f", composeFile, "ps", "-q")
|
||||||
|
hideWindow(cmd)
|
||||||
out, err = cmd.Output()
|
out, err = cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "error"
|
return "error"
|
||||||
|
|||||||
+9
-2
@@ -42,6 +42,14 @@ func main() {
|
|||||||
log.Fatalf("Cannot create data-dir: %v", err)
|
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)
|
cfg, _, err := loadOrCreateConfig(*dataDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Cannot load config: %v", err)
|
log.Fatalf("Cannot load config: %v", err)
|
||||||
@@ -78,8 +86,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startTailscaleAndReport(dataDir, nodeID, headscaleURL, authKey string) {
|
func startTailscaleAndReport(dataDir, nodeID, headscaleURL, authKey string) {
|
||||||
tsDir := filepath.Join(dataDir, "tailscale")
|
ip, err := startTailscale(dataDir, nodeID, headscaleURL, authKey)
|
||||||
ip, err := startTailscale(tsDir, nodeID, headscaleURL, authKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Tailscale error: %v", err)
|
log.Printf("Tailscale error: %v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
+3
-1
@@ -80,7 +80,9 @@ func openBrowser(url string) {
|
|||||||
args = []string{url}
|
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)
|
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 {
|
if err := os.MkdirAll(tsDataDir, 0700); err != nil {
|
||||||
return "", fmt.Errorf("create tailscale dir: %w", err)
|
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")
|
tsSocket = filepath.Join(tsDataDir, "tailscaled.sock")
|
||||||
|
}
|
||||||
stateFile := filepath.Join(tsDataDir, "tailscaled.state")
|
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"),
|
tsCmd = exec.Command(tailscaleBin("tailscaled"),
|
||||||
"--state="+stateFile,
|
"--state="+stateFile,
|
||||||
"--socket="+tsSocket,
|
"--socket="+tsSocket,
|
||||||
"--tun=userspace-networking",
|
"--tun=userspace-networking",
|
||||||
)
|
)
|
||||||
tsCmd.Stdout = os.Stdout
|
hideWindow(tsCmd)
|
||||||
tsCmd.Stderr = os.Stderr
|
// 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 {
|
if err := tsCmd.Start(); err != nil {
|
||||||
tsCmd = nil
|
tsCmd = nil
|
||||||
return "", fmt.Errorf("start tailscaled: %w", err)
|
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)
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
upCmd := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
upArgs := []string{
|
||||||
"--socket=" + tsSocket,
|
"--socket=" + tsSocket,
|
||||||
"up",
|
"up",
|
||||||
"--authkey=" + authKey,
|
"--authkey=" + authKey,
|
||||||
"--login-server=" + headscaleURL,
|
"--login-server=" + headscaleURL,
|
||||||
"--hostname=" + nodeID,
|
"--hostname=" + nodeID,
|
||||||
"--accept-dns=false",
|
"--accept-dns=false",
|
||||||
"--operator=root",
|
}
|
||||||
)
|
if runtime.GOOS != "windows" {
|
||||||
upCmd.Stdout = os.Stdout
|
// --operator is only meaningful on Unix systems.
|
||||||
upCmd.Stderr = os.Stderr
|
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 {
|
if err := upCmd.Run(); err != nil {
|
||||||
_ = tsCmd.Process.Kill()
|
_ = tsCmd.Process.Kill()
|
||||||
_ = tsCmd.Wait()
|
_ = tsCmd.Wait()
|
||||||
@@ -104,10 +121,12 @@ func startTailscale(dataDir, nodeID, headscaleURL, authKey string) (string, erro
|
|||||||
|
|
||||||
// Wait for an IP address.
|
// Wait for an IP address.
|
||||||
for {
|
for {
|
||||||
out, err := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
statusCmd := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
||||||
"--socket="+tsSocket,
|
"--socket="+tsSocket,
|
||||||
"status", "--json",
|
"status", "--json",
|
||||||
).Output()
|
)
|
||||||
|
hideWindow(statusCmd)
|
||||||
|
out, err := statusCmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -151,7 +170,9 @@ func stopTailscaleLocked() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if tsSocket != "" {
|
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.Process.Kill()
|
||||||
_ = tsCmd.Wait()
|
_ = tsCmd.Wait()
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ func startUI(dataDir, nodeID, serverAddr string) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
go func() {
|
go func() {
|
||||||
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||||
|
hideWindow(cmd)
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
|
|||||||
Reference in New Issue
Block a user