3-14-26 updates
This commit is contained in:
parent
243a201a64
commit
6019d0bc1c
@ -32,6 +32,8 @@ const (
|
|||||||
shadowMetadataName = "metadata.json"
|
shadowMetadataName = "metadata.json"
|
||||||
MaxModUploadSize = 250 * 1024 * 1024
|
MaxModUploadSize = 250 * 1024 * 1024
|
||||||
MaxDataPackSize = 100 * 1024 * 1024
|
MaxDataPackSize = 100 * 1024 * 1024
|
||||||
|
MaxDevUploadSize = 250 * 1024 * 1024
|
||||||
|
DevWorkspaceRoot = "/home/dev/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -96,6 +98,9 @@ type Meta struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RuntimeRoot(cfg *state.Config) string {
|
func RuntimeRoot(cfg *state.Config) string {
|
||||||
|
if cfg != nil && strings.EqualFold(cfg.ContainerType, "dev") {
|
||||||
|
return DevWorkspaceRoot
|
||||||
|
}
|
||||||
return mods.ResolveServerRoot(cfg)
|
return mods.ResolveServerRoot(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,7 +234,7 @@ func List(root, rel string) (ListResponse, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Stat(root, rel string) (StatResponse, error) {
|
func Stat(containerType, root, rel string) (StatResponse, error) {
|
||||||
resolvedPath, responsePath, err := ResolveWorldPath(root, rel)
|
resolvedPath, responsePath, err := ResolveWorldPath(root, rel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return StatResponse{}, err
|
return StatResponse{}, err
|
||||||
@ -255,7 +260,7 @@ func Stat(root, rel string) (StatResponse, error) {
|
|||||||
Type: entryType,
|
Type: entryType,
|
||||||
Size: info.Size(),
|
Size: info.Size(),
|
||||||
Modified: info.ModTime().UTC().Format(time.RFC3339),
|
Modified: info.ModTime().UTC().Format(time.RFC3339),
|
||||||
IsWritable: isWritablePath(root, responsePath),
|
IsWritable: isWritablePath(containerType, root, responsePath),
|
||||||
HasBackup: hasBackup(root, responsePath),
|
HasBackup: hasBackup(root, responsePath),
|
||||||
Source: sourceForPath(root, responsePath),
|
Source: sourceForPath(root, responsePath),
|
||||||
}, nil
|
}, nil
|
||||||
@ -317,7 +322,7 @@ func OpenDownload(root, rel string) (*os.File, os.FileInfo, string, error) {
|
|||||||
return file, info, filepath.Base(resolvedPath), nil
|
return file, info, filepath.Base(resolvedPath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Delete(root, rel string) (string, error) {
|
func Delete(containerType, root, rel string) (string, error) {
|
||||||
resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root))
|
resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -327,7 +332,7 @@ func Delete(root, rel string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if !isAllowedDelete(normalizedRel) {
|
if !isAllowedDelete(containerType, normalizedRel) {
|
||||||
return "", ErrDeleteDenied
|
return "", ErrDeleteDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +369,7 @@ func Delete(root, rel string) (string, error) {
|
|||||||
return normalizedRel, nil
|
return normalizedRel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Write(root, rel string, data []byte) (bool, error) {
|
func Write(containerType, root, rel string, data []byte) (bool, error) {
|
||||||
resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root))
|
resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -374,7 +379,7 @@ func Write(root, rel string, data []byte) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if !isAllowedWrite(normalizedRel) {
|
if !isAllowedWrite(containerType, normalizedRel) {
|
||||||
return false, ErrWriteDenied
|
return false, ErrWriteDenied
|
||||||
}
|
}
|
||||||
if len(data) > MaxWriteSize {
|
if len(data) > MaxWriteSize {
|
||||||
@ -415,7 +420,7 @@ func Write(root, rel string, data []byte) (bool, error) {
|
|||||||
return backupCreated, nil
|
return backupCreated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Revert(root, rel string) (string, error) {
|
func Revert(containerType, root, rel string) (string, error) {
|
||||||
resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root))
|
resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -425,7 +430,7 @@ func Revert(root, rel string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if !isAllowedWrite(normalizedRel) {
|
if !isAllowedWrite(containerType, normalizedRel) {
|
||||||
return "", ErrWriteDenied
|
return "", ErrWriteDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,7 +471,7 @@ func Revert(root, rel string) (string, error) {
|
|||||||
return normalizedRel, nil
|
return normalizedRel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Upload(root, rel string, r io.Reader, sizeLimit int64, overwrite bool) (int64, bool, error) {
|
func Upload(containerType, root, rel string, r io.Reader, sizeLimit int64, overwrite bool) (int64, bool, error) {
|
||||||
resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root))
|
resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, false, err
|
return 0, false, err
|
||||||
@ -476,7 +481,7 @@ func Upload(root, rel string, r io.Reader, sizeLimit int64, overwrite bool) (int
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, false, err
|
return 0, false, err
|
||||||
}
|
}
|
||||||
allowedLimit, ok := uploadLimitForPath(normalizedRel)
|
allowedLimit, ok := uploadLimitForPath(containerType, normalizedRel)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, false, ErrUploadDenied
|
return 0, false, ErrUploadDenied
|
||||||
}
|
}
|
||||||
@ -600,7 +605,10 @@ func isBinary(data []byte) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAllowedDelete(rel string) bool {
|
func isAllowedDelete(containerType, rel string) bool {
|
||||||
|
if strings.EqualFold(containerType, "dev") {
|
||||||
|
return rel != ""
|
||||||
|
}
|
||||||
parts := strings.Split(rel, "/")
|
parts := strings.Split(rel, "/")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return false
|
return false
|
||||||
@ -615,7 +623,10 @@ func isAllowedDelete(rel string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAllowedWrite(rel string) bool {
|
func isAllowedWrite(containerType, rel string) bool {
|
||||||
|
if strings.EqualFold(containerType, "dev") {
|
||||||
|
return rel != ""
|
||||||
|
}
|
||||||
parts := strings.Split(rel, "/")
|
parts := strings.Split(rel, "/")
|
||||||
if len(parts) == 1 {
|
if len(parts) == 1 {
|
||||||
return parts[0] == "server.properties"
|
return parts[0] == "server.properties"
|
||||||
@ -632,7 +643,10 @@ func isAllowedWrite(rel string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadLimitForPath(rel string) (int64, bool) {
|
func uploadLimitForPath(containerType, rel string) (int64, bool) {
|
||||||
|
if strings.EqualFold(containerType, "dev") {
|
||||||
|
return MaxDevUploadSize, rel != ""
|
||||||
|
}
|
||||||
parts := strings.Split(rel, "/")
|
parts := strings.Split(rel, "/")
|
||||||
switch {
|
switch {
|
||||||
case len(parts) == 2 && parts[0] == "mods" && strings.HasSuffix(strings.ToLower(parts[1]), ".jar"):
|
case len(parts) == 2 && parts[0] == "mods" && strings.HasSuffix(strings.ToLower(parts[1]), ".jar"):
|
||||||
@ -644,8 +658,8 @@ func uploadLimitForPath(rel string) (int64, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isWritablePath(root, rel string) bool {
|
func isWritablePath(containerType, root, rel string) bool {
|
||||||
if !isAllowedWrite(rel) {
|
if !isAllowedWrite(containerType, rel) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
agentfiles "zlh-agent/internal/files"
|
agentfiles "zlh-agent/internal/files"
|
||||||
|
"zlh-agent/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleGameFilesList(w http.ResponseWriter, r *http.Request) {
|
func HandleGameFilesList(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -18,7 +19,7 @@ func HandleGameFilesList(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, serverRoot, ok := requireMinecraftGame(w)
|
_, serverRoot, ok := requireFileContainer(w)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -37,12 +38,12 @@ func HandleGameFilesStat(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, serverRoot, ok := requireMinecraftGame(w)
|
cfg, serverRoot, ok := requireFileContainer(w)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := agentfiles.Stat(serverRoot, r.URL.Query().Get("path"))
|
resp, err := agentfiles.Stat(cfg.ContainerType, serverRoot, r.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeFilesError(w, err)
|
writeFilesError(w, err)
|
||||||
return
|
return
|
||||||
@ -56,7 +57,7 @@ func HandleGameFilesRead(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, serverRoot, ok := requireMinecraftGame(w)
|
_, serverRoot, ok := requireFileContainer(w)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -75,7 +76,7 @@ func HandleGameFilesDownload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, serverRoot, ok := requireMinecraftGame(w)
|
_, serverRoot, ok := requireFileContainer(w)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -109,7 +110,7 @@ func HandleGameFilesUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, serverRoot, ok := requireMinecraftGame(w)
|
cfg, serverRoot, ok := requireFileContainer(w)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -147,7 +148,7 @@ func HandleGameFilesUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
size, overwritten, err := agentfiles.Upload(serverRoot, normalizedPath, part, 0, overwrite)
|
size, overwritten, err := agentfiles.Upload(cfg.ContainerType, serverRoot, normalizedPath, part, 0, overwrite)
|
||||||
part.Close()
|
part.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeFilesError(w, err)
|
writeFilesError(w, err)
|
||||||
@ -169,12 +170,12 @@ func HandleGameFilesRevert(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, serverRoot, ok := requireMinecraftGame(w)
|
cfg, serverRoot, ok := requireFileContainer(w)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
revertedPath, err := agentfiles.Revert(serverRoot, r.URL.Query().Get("path"))
|
revertedPath, err := agentfiles.Revert(cfg.ContainerType, serverRoot, r.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeFilesError(w, err)
|
writeFilesError(w, err)
|
||||||
return
|
return
|
||||||
@ -186,12 +187,12 @@ func HandleGameFilesRevert(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleGameFilesDelete(w http.ResponseWriter, r *http.Request) {
|
func handleGameFilesDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
_, serverRoot, ok := requireMinecraftGame(w)
|
cfg, serverRoot, ok := requireFileContainer(w)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
deletedPath, err := agentfiles.Delete(serverRoot, r.URL.Query().Get("path"))
|
deletedPath, err := agentfiles.Delete(cfg.ContainerType, serverRoot, r.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeFilesError(w, err)
|
writeFilesError(w, err)
|
||||||
return
|
return
|
||||||
@ -203,7 +204,7 @@ func handleGameFilesDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleGameFilesWrite(w http.ResponseWriter, r *http.Request) {
|
func handleGameFilesWrite(w http.ResponseWriter, r *http.Request) {
|
||||||
_, serverRoot, ok := requireMinecraftGame(w)
|
cfg, serverRoot, ok := requireFileContainer(w)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -220,7 +221,7 @@ func handleGameFilesWrite(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSONError(w, http.StatusBadRequest, "invalid request body")
|
writeJSONError(w, http.StatusBadRequest, "invalid request body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backupCreated, err := agentfiles.Write(serverRoot, normalizedPath, data)
|
backupCreated, err := agentfiles.Write(cfg.ContainerType, serverRoot, normalizedPath, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeFilesError(w, err)
|
writeFilesError(w, err)
|
||||||
return
|
return
|
||||||
@ -252,3 +253,19 @@ func writeFilesError(w http.ResponseWriter, err error) {
|
|||||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requireFileContainer(w http.ResponseWriter) (*state.Config, string, bool) {
|
||||||
|
cfg, err := state.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "no config loaded")
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(cfg.ContainerType) {
|
||||||
|
case "game", "dev":
|
||||||
|
return cfg, agentfiles.RuntimeRoot(cfg), true
|
||||||
|
default:
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "unsupported container type")
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import (
|
|||||||
mcstatus "zlh-agent/internal/minecraft"
|
mcstatus "zlh-agent/internal/minecraft"
|
||||||
"zlh-agent/internal/provision"
|
"zlh-agent/internal/provision"
|
||||||
"zlh-agent/internal/provision/devcontainer"
|
"zlh-agent/internal/provision/devcontainer"
|
||||||
|
"zlh-agent/internal/provision/devcontainer/dotnet"
|
||||||
"zlh-agent/internal/provision/devcontainer/go"
|
"zlh-agent/internal/provision/devcontainer/go"
|
||||||
"zlh-agent/internal/provision/devcontainer/java"
|
"zlh-agent/internal/provision/devcontainer/java"
|
||||||
"zlh-agent/internal/provision/devcontainer/node"
|
"zlh-agent/internal/provision/devcontainer/node"
|
||||||
@ -28,6 +29,8 @@ import (
|
|||||||
"zlh-agent/internal/version"
|
"zlh-agent/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ReadinessTimeout = 60 * time.Second
|
||||||
|
|
||||||
/*
|
/*
|
||||||
--------------------------------------------------------------------------
|
--------------------------------------------------------------------------
|
||||||
Helpers
|
Helpers
|
||||||
@ -56,7 +59,7 @@ func waitMinecraftReady(cfg *state.Config, phase string, started time.Time) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
lifecycleLog(cfg, phase, 1, started, "probe_begin")
|
lifecycleLog(cfg, phase, 1, started, "probe_begin")
|
||||||
if err := mcstatus.WaitUntilReady(*cfg, 60*time.Second, 3*time.Second); err != nil {
|
if err := mcstatus.WaitUntilReady(*cfg, ReadinessTimeout, 3*time.Second); err != nil {
|
||||||
state.SetReadyState(false, "minecraft_ping", err.Error())
|
state.SetReadyState(false, "minecraft_ping", err.Error())
|
||||||
lifecycleLog(cfg, phase, 1, started, "probe_timeout err=%v", err)
|
lifecycleLog(cfg, phase, 1, started, "probe_timeout err=%v", err)
|
||||||
return err
|
return err
|
||||||
@ -105,7 +108,7 @@ func ensureProvisioned(cfg *state.Config) error {
|
|||||||
|
|
||||||
if cfg.ContainerType == "dev" {
|
if cfg.ContainerType == "dev" {
|
||||||
|
|
||||||
if !devcontainer.IsProvisioned() {
|
if !devcontainer.IsProvisioned() || !devcontainer.RuntimeInstalled(cfg.Runtime, cfg.Version) {
|
||||||
if err := runProvisionPipeline(cfg); err != nil {
|
if err := runProvisionPipeline(cfg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -122,6 +125,8 @@ func ensureProvisioned(cfg *state.Config) error {
|
|||||||
err = goenv.Verify(*cfg)
|
err = goenv.Verify(*cfg)
|
||||||
case "java":
|
case "java":
|
||||||
err = java.Verify(*cfg)
|
err = java.Verify(*cfg)
|
||||||
|
case "dotnet":
|
||||||
|
err = dotnet.Verify(*cfg)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported devcontainer runtime: %s", cfg.Runtime)
|
return fmt.Errorf("unsupported devcontainer runtime: %s", cfg.Runtime)
|
||||||
}
|
}
|
||||||
@ -192,12 +197,12 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func(c state.Config) {
|
go func(c state.Config) {
|
||||||
log.Println("[agent] async provision+start begin")
|
log.Printf("[http] vmid=%d async provision+start begin", c.VMID)
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
lifecycleLog(&c, "config_async", 1, started, "begin")
|
lifecycleLog(&c, "config_async", 1, started, "begin")
|
||||||
|
|
||||||
if err := ensureProvisioned(&c); err != nil {
|
if err := ensureProvisioned(&c); err != nil {
|
||||||
log.Println("[agent] provision error:", err)
|
log.Printf("[http] vmid=%d provision error: %v", c.VMID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +211,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
state.SetReadyState(false, "", "")
|
state.SetReadyState(false, "", "")
|
||||||
lifecycleLog(&c, "start", 1, started, "start_requested")
|
lifecycleLog(&c, "start", 1, started, "start_requested")
|
||||||
if err := system.StartServer(&c); err != nil {
|
if err := system.StartServer(&c); err != nil {
|
||||||
log.Println("[agent] start error:", err)
|
log.Printf("[http] vmid=%d start error: %v", c.VMID, err)
|
||||||
state.SetError(err)
|
state.SetError(err)
|
||||||
state.SetState(state.StateError)
|
state.SetState(state.StateError)
|
||||||
lifecycleLog(&c, "start", 1, started, "start_failed err=%v", err)
|
lifecycleLog(&c, "start", 1, started, "start_failed err=%v", err)
|
||||||
@ -237,7 +242,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
if time.Now().After(propsDeadline) {
|
if time.Now().After(propsDeadline) {
|
||||||
err := fmt.Errorf("forge server.properties not found before timeout")
|
err := fmt.Errorf("forge server.properties not found before timeout")
|
||||||
log.Println("[agent] forge post-start error:", err)
|
log.Printf("[http] vmid=%d forge post-start error: %v", c.VMID, err)
|
||||||
state.SetError(err)
|
state.SetError(err)
|
||||||
state.SetState(state.StateError)
|
state.SetState(state.StateError)
|
||||||
return
|
return
|
||||||
@ -247,7 +252,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
_ = system.StopServer()
|
_ = system.StopServer()
|
||||||
if err := system.WaitForServerExit(20 * time.Second); err != nil {
|
if err := system.WaitForServerExit(20 * time.Second); err != nil {
|
||||||
log.Println("[agent] forge stop wait error:", err)
|
log.Printf("[http] vmid=%d forge stop wait error: %v", c.VMID, err)
|
||||||
state.SetError(err)
|
state.SetError(err)
|
||||||
state.SetState(state.StateError)
|
state.SetState(state.StateError)
|
||||||
lifecycleLog(&c, "forge_post", 1, started, "stop_wait_failed err=%v", err)
|
lifecycleLog(&c, "forge_post", 1, started, "stop_wait_failed err=%v", err)
|
||||||
@ -255,7 +260,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := minecraft.EnforceForgeServerProperties(c); err != nil {
|
if err := minecraft.EnforceForgeServerProperties(c); err != nil {
|
||||||
log.Println("[agent] forge post-start error:", err)
|
log.Printf("[http] vmid=%d forge post-start error: %v", c.VMID, err)
|
||||||
state.SetError(err)
|
state.SetError(err)
|
||||||
state.SetState(state.StateError)
|
state.SetState(state.StateError)
|
||||||
lifecycleLog(&c, "forge_post", 1, started, "enforce_failed err=%v", err)
|
lifecycleLog(&c, "forge_post", 1, started, "enforce_failed err=%v", err)
|
||||||
@ -265,7 +270,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
state.SetState(state.StateStarting)
|
state.SetState(state.StateStarting)
|
||||||
state.SetReadyState(false, "", "")
|
state.SetReadyState(false, "", "")
|
||||||
if err := system.StartServer(&c); err != nil {
|
if err := system.StartServer(&c); err != nil {
|
||||||
log.Println("[agent] restart error:", err)
|
log.Printf("[http] vmid=%d restart error: %v", c.VMID, err)
|
||||||
state.SetError(err)
|
state.SetError(err)
|
||||||
state.SetState(state.StateError)
|
state.SetState(state.StateError)
|
||||||
lifecycleLog(&c, "forge_post", 1, started, "restart_failed err=%v", err)
|
lifecycleLog(&c, "forge_post", 1, started, "restart_failed err=%v", err)
|
||||||
@ -280,7 +285,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("[agent] async provision+start complete")
|
log.Printf("[http] vmid=%d async provision+start complete", c.VMID)
|
||||||
}(cfg)
|
}(cfg)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@ -395,6 +400,20 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
if t := state.GetLastReadyAt(); !t.IsZero() {
|
if t := state.GetLastReadyAt(); !t.IsZero() {
|
||||||
readyAt = t.UTC().Format(time.RFC3339)
|
readyAt = t.UTC().Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
lastCrashTime := ""
|
||||||
|
lastCrashExitCode := 0
|
||||||
|
lastCrashSignal := 0
|
||||||
|
lastCrashUptimeSeconds := int64(0)
|
||||||
|
var lastCrashLogTail []string
|
||||||
|
if crash := state.GetLastCrash(); crash != nil {
|
||||||
|
if !crash.Time.IsZero() {
|
||||||
|
lastCrashTime = crash.Time.UTC().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
lastCrashExitCode = crash.ExitCode
|
||||||
|
lastCrashSignal = crash.Signal
|
||||||
|
lastCrashUptimeSeconds = crash.UptimeSeconds
|
||||||
|
lastCrashLogTail = crash.LogTail
|
||||||
|
}
|
||||||
|
|
||||||
resp := map[string]any{
|
resp := map[string]any{
|
||||||
"state": state.GetState(),
|
"state": state.GetState(),
|
||||||
@ -405,6 +424,11 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
"lastReadyAt": readyAt,
|
"lastReadyAt": readyAt,
|
||||||
"installStep": state.GetInstallStep(),
|
"installStep": state.GetInstallStep(),
|
||||||
"crashCount": state.GetCrashCount(),
|
"crashCount": state.GetCrashCount(),
|
||||||
|
"lastCrashTime": lastCrashTime,
|
||||||
|
"lastCrashExitCode": lastCrashExitCode,
|
||||||
|
"lastCrashSignal": lastCrashSignal,
|
||||||
|
"lastCrashUptimeSeconds": lastCrashUptimeSeconds,
|
||||||
|
"lastCrashLogTail": lastCrashLogTail,
|
||||||
"error": nil,
|
"error": nil,
|
||||||
"config": cfg,
|
"config": cfg,
|
||||||
"timestamp": time.Now().Unix(),
|
"timestamp": time.Now().Unix(),
|
||||||
|
|||||||
@ -57,11 +57,11 @@ func getConsoleSession(cfg *state.Config) (*consoleSession, bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sess.ptyFile != currentPTY {
|
if sess.ptyFile != currentPTY {
|
||||||
log.Printf("[ws] pty changed, destroying stale session: vmid=%d type=%s", cfg.VMID, cfg.ContainerType)
|
log.Printf("[console] vmid=%d type=%s pty changed, destroying stale session", cfg.VMID, cfg.ContainerType)
|
||||||
sess.destroy()
|
sess.destroy()
|
||||||
} else {
|
} else {
|
||||||
sess.touch()
|
sess.touch()
|
||||||
log.Printf("[ws] session reuse: vmid=%d type=%s", cfg.VMID, cfg.ContainerType)
|
log.Printf("[console] vmid=%d type=%s session reuse", cfg.VMID, cfg.ContainerType)
|
||||||
return sess, true, nil
|
return sess, true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ func getConsoleSession(cfg *state.Config) (*consoleSession, bool, error) {
|
|||||||
sessions[key] = sess
|
sessions[key] = sess
|
||||||
sessionMu.Unlock()
|
sessionMu.Unlock()
|
||||||
|
|
||||||
log.Printf("[ws] session created: vmid=%d type=%s", cfg.VMID, cfg.ContainerType)
|
log.Printf("[console] vmid=%d type=%s session created", cfg.VMID, cfg.ContainerType)
|
||||||
return sess, false, nil
|
return sess, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ func (s *consoleSession) addConn(conn *websocket.Conn, cc *consoleConn) *console
|
|||||||
}
|
}
|
||||||
s.conns[conn] = cc
|
s.conns[conn] = cc
|
||||||
s.lastActive = time.Now()
|
s.lastActive = time.Now()
|
||||||
log.Printf("[ws] conn add: vmid=%d type=%s conns=%d", s.cfg.VMID, s.cfg.ContainerType, len(s.conns))
|
log.Printf("[console] vmid=%d type=%s conn add conns=%d", s.cfg.VMID, s.cfg.ContainerType, len(s.conns))
|
||||||
return cc
|
return cc
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ func (s *consoleSession) removeConn(conn *websocket.Conn) int {
|
|||||||
safeCloseChan(cc.send)
|
safeCloseChan(cc.send)
|
||||||
}
|
}
|
||||||
s.lastActive = time.Now()
|
s.lastActive = time.Now()
|
||||||
log.Printf("[ws] conn remove: vmid=%d type=%s conns=%d", s.cfg.VMID, s.cfg.ContainerType, len(s.conns))
|
log.Printf("[console] vmid=%d type=%s conn remove conns=%d", s.cfg.VMID, s.cfg.ContainerType, len(s.conns))
|
||||||
return len(s.conns)
|
return len(s.conns)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,14 +133,14 @@ func (s *consoleSession) startReader() {
|
|||||||
if n > 0 {
|
if n > 0 {
|
||||||
out := make([]byte, n)
|
out := make([]byte, n)
|
||||||
copy(out, buf[:n])
|
copy(out, buf[:n])
|
||||||
log.Printf("[ws] pty read: vmid=%d bytes=%d", s.cfg.VMID, n)
|
log.Printf("[console] vmid=%d pty read bytes=%d", s.cfg.VMID, n)
|
||||||
s.broadcast(out)
|
s.broadcast(out)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
log.Printf("[ws] pty read loop exit: vmid=%d err=EOF", s.cfg.VMID)
|
log.Printf("[console] vmid=%d pty read loop exit err=EOF", s.cfg.VMID)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[ws] pty read loop exit: vmid=%d err=%v", s.cfg.VMID, err)
|
log.Printf("[console] vmid=%d pty read loop exit err=%v", s.cfg.VMID, err)
|
||||||
}
|
}
|
||||||
s.destroy()
|
s.destroy()
|
||||||
return
|
return
|
||||||
@ -194,7 +194,7 @@ func (s *consoleSession) scheduleCleanupIfIdle() {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
if conns == 0 && lastActive.Equal(ts) {
|
if conns == 0 && lastActive.Equal(ts) {
|
||||||
log.Printf("[ws] session cleanup: vmid=%d type=%s", s.cfg.VMID, s.cfg.ContainerType)
|
log.Printf("[console] vmid=%d type=%s session cleanup", s.cfg.VMID, s.cfg.ContainerType)
|
||||||
if s.cfg.ContainerType == "dev" {
|
if s.cfg.ContainerType == "dev" {
|
||||||
_ = system.StopDevShell()
|
_ = system.StopDevShell()
|
||||||
}
|
}
|
||||||
@ -223,7 +223,7 @@ func (s *consoleSession) destroy() {
|
|||||||
delete(sessions, s.key)
|
delete(sessions, s.key)
|
||||||
sessionMu.Unlock()
|
sessionMu.Unlock()
|
||||||
|
|
||||||
log.Printf("[ws] session destroyed: vmid=%d type=%s", s.cfg.VMID, s.cfg.ContainerType)
|
log.Printf("[console] vmid=%d type=%s session destroyed", s.cfg.VMID, s.cfg.ContainerType)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,17 +5,10 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"zlh-agent/internal/provision/executil"
|
"zlh-agent/internal/provision/executil"
|
||||||
"zlh-agent/internal/provision/markers"
|
|
||||||
"zlh-agent/internal/state"
|
"zlh-agent/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Install(cfg state.Config) error {
|
func Install(cfg state.Config) error {
|
||||||
const marker = "addon-codeserver"
|
|
||||||
|
|
||||||
if markers.IsPresent(marker) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptPath := filepath.Join(
|
scriptPath := filepath.Join(
|
||||||
executil.ScriptsRoot,
|
executil.ScriptsRoot,
|
||||||
"addons",
|
"addons",
|
||||||
@ -26,6 +19,5 @@ func Install(cfg state.Config) error {
|
|||||||
if err := executil.RunScript(scriptPath); err != nil {
|
if err := executil.RunScript(scriptPath); err != nil {
|
||||||
return fmt.Errorf("codeserver install failed: %w", err)
|
return fmt.Errorf("codeserver install failed: %w", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return markers.Write(marker)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,19 @@ package devcontainer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"zlh-agent/internal/provcommon"
|
||||||
|
"zlh-agent/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -19,10 +29,32 @@ const (
|
|||||||
// MarkerDir is where devcontainer state is stored.
|
// MarkerDir is where devcontainer state is stored.
|
||||||
MarkerDir = "/opt/zlh/.zlh"
|
MarkerDir = "/opt/zlh/.zlh"
|
||||||
|
|
||||||
|
// WorkspaceDir is the root working directory exposed to dev containers.
|
||||||
|
WorkspaceDir = "/home/dev/workspace"
|
||||||
|
|
||||||
|
// Dev user environment
|
||||||
|
DevUser = "dev"
|
||||||
|
DevHome = "/home/dev"
|
||||||
|
|
||||||
|
// CatalogRelativePath is the artifact-server path to the dev runtime catalog.
|
||||||
|
CatalogRelativePath = "devcontainer/_catalog.json"
|
||||||
|
|
||||||
|
// RuntimeRoot is where versioned dev runtimes are installed.
|
||||||
|
RuntimeRoot = "/opt/zlh/runtimes"
|
||||||
|
|
||||||
// ReadyMarker is written after a dev container is fully provisioned.
|
// ReadyMarker is written after a dev container is fully provisioned.
|
||||||
ReadyMarker = "devcontainer_ready.json"
|
ReadyMarker = "devcontainer_ready.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Catalog struct {
|
||||||
|
Runtimes []CatalogRuntime `json:"runtimes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CatalogRuntime struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Versions []string `json:"versions"`
|
||||||
|
}
|
||||||
|
|
||||||
// ReadyMarkerPath returns the absolute path to the ready marker file.
|
// ReadyMarkerPath returns the absolute path to the ready marker file.
|
||||||
func ReadyMarkerPath() string {
|
func ReadyMarkerPath() string {
|
||||||
return filepath.Join(MarkerDir, ReadyMarker)
|
return filepath.Join(MarkerDir, ReadyMarker)
|
||||||
@ -54,3 +86,110 @@ func WriteReadyMarker(runtime string) error {
|
|||||||
|
|
||||||
return os.WriteFile(ReadyMarkerPath(), raw, 0644)
|
return os.WriteFile(ReadyMarkerPath(), raw, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureWorkspace makes sure the dev workspace root exists before shell/filesystem access.
|
||||||
|
func EnsureWorkspace() error {
|
||||||
|
return os.MkdirAll(WorkspaceDir, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadCatalog() (*Catalog, error) {
|
||||||
|
url := provcommon.BuildArtifactURL(CatalogRelativePath)
|
||||||
|
resp, err := (&http.Client{Timeout: 15 * time.Second}).Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetch dev runtime catalog: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("fetch dev runtime catalog: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read dev runtime catalog: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var catalog Catalog
|
||||||
|
if err := json.Unmarshal(raw, &catalog); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse dev runtime catalog: %w", err)
|
||||||
|
}
|
||||||
|
return &catalog, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateRuntimeSelection(cfg state.Config) error {
|
||||||
|
catalog, err := LoadCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimeID := strings.ToLower(strings.TrimSpace(cfg.Runtime))
|
||||||
|
version := strings.TrimSpace(cfg.Version)
|
||||||
|
if runtimeID == "" {
|
||||||
|
return fmt.Errorf("dev runtime missing")
|
||||||
|
}
|
||||||
|
if version == "" {
|
||||||
|
return fmt.Errorf("dev runtime version missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, runtime := range catalog.Runtimes {
|
||||||
|
if strings.EqualFold(runtime.ID, runtimeID) {
|
||||||
|
for _, candidate := range runtime.Versions {
|
||||||
|
if candidate == version {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unsupported %s version: %s", runtimeID, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unsupported dev runtime: %s", runtimeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RuntimeInstallDir(runtimeName, version string) string {
|
||||||
|
return filepath.Join(RuntimeRoot, strings.ToLower(strings.TrimSpace(runtimeName)), strings.TrimSpace(version))
|
||||||
|
}
|
||||||
|
|
||||||
|
func RuntimeInstalled(runtimeName, version string) bool {
|
||||||
|
info, err := os.Stat(RuntimeInstallDir(runtimeName, version))
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RuntimeMarker(runtimeName, version string) string {
|
||||||
|
return fmt.Sprintf("devcontainer-%s-%s", strings.ToLower(strings.TrimSpace(runtimeName)), strings.ReplaceAll(strings.TrimSpace(version), "/", "_"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureDevUserEnvironment() error {
|
||||||
|
if _, err := user.Lookup(DevUser); err != nil {
|
||||||
|
if err := exec.Command("useradd", "-m", "-s", "/bin/bash", DevUser).Run(); err != nil {
|
||||||
|
return fmt.Errorf("create dev user: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(WorkspaceDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("create workspace: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := user.Lookup(DevUser)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("lookup dev user: %w", err)
|
||||||
|
}
|
||||||
|
uid, err := strconv.Atoi(u.Uid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse dev uid: %w", err)
|
||||||
|
}
|
||||||
|
gid, err := strconv.Atoi(u.Gid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse dev gid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := filepath.Walk(DevHome, func(path string, info os.FileInfo, walkErr error) error {
|
||||||
|
if walkErr != nil {
|
||||||
|
return walkErr
|
||||||
|
}
|
||||||
|
return os.Chown(path, uid, gid)
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("chown dev home: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"zlh-agent/internal/state"
|
"zlh-agent/internal/state"
|
||||||
|
|
||||||
|
"zlh-agent/internal/provision/devcontainer/dotnet"
|
||||||
devgo "zlh-agent/internal/provision/devcontainer/go"
|
devgo "zlh-agent/internal/provision/devcontainer/go"
|
||||||
"zlh-agent/internal/provision/devcontainer/java"
|
"zlh-agent/internal/provision/devcontainer/java"
|
||||||
"zlh-agent/internal/provision/devcontainer/node"
|
"zlh-agent/internal/provision/devcontainer/node"
|
||||||
@ -13,18 +14,36 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Provision(cfg state.Config) error {
|
func Provision(cfg state.Config) error {
|
||||||
|
if err := ValidateRuntimeSelection(cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := EnsureDevUserEnvironment(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
runtime := strings.ToLower(cfg.Runtime)
|
runtime := strings.ToLower(cfg.Runtime)
|
||||||
|
|
||||||
|
var err error
|
||||||
switch runtime {
|
switch runtime {
|
||||||
case "node":
|
case "node":
|
||||||
return node.Install(cfg)
|
err = node.Install(cfg)
|
||||||
case "python":
|
case "python":
|
||||||
return python.Install(cfg)
|
err = python.Install(cfg)
|
||||||
case "go":
|
case "go":
|
||||||
return devgo.Install(cfg)
|
err = devgo.Install(cfg)
|
||||||
case "java":
|
case "java":
|
||||||
return java.Install(cfg)
|
err = java.Install(cfg)
|
||||||
|
case "dotnet":
|
||||||
|
err = dotnet.Install(cfg)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported dev container runtime: %s", runtime)
|
return fmt.Errorf("unsupported dev container runtime: %s", runtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := WriteReadyMarker(runtime); err != nil {
|
||||||
|
return fmt.Errorf("write dev ready marker: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
45
internal/provision/devcontainer/dotnet/install.go
Normal file
45
internal/provision/devcontainer/dotnet/install.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package dotnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"zlh-agent/internal/provision/executil"
|
||||||
|
"zlh-agent/internal/provision/markers"
|
||||||
|
"zlh-agent/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Install(cfg state.Config) error {
|
||||||
|
marker := runtimeMarker(cfg.Version)
|
||||||
|
if runtimeInstalled(cfg.Version) {
|
||||||
|
if !markers.IsPresent(marker) {
|
||||||
|
return markers.Write(marker)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if markers.IsPresent(marker) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := executil.RunEmbeddedScript(
|
||||||
|
"devcontainer/dotnet/install.sh",
|
||||||
|
"RUNTIME=dotnet",
|
||||||
|
"ARCHIVE_EXT=tar.gz",
|
||||||
|
"RUNTIME_VERSION="+cfg.Version,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("dotnet devcontainer install failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers.Write(marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runtimeInstalled(version string) bool {
|
||||||
|
info, err := os.Stat(filepath.Join("/opt/zlh/runtimes/dotnet", strings.TrimSpace(version)))
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runtimeMarker(version string) string {
|
||||||
|
return "devcontainer-dotnet-" + strings.ReplaceAll(strings.TrimSpace(version), "/", "_")
|
||||||
|
}
|
||||||
23
internal/provision/devcontainer/dotnet/verify.go
Normal file
23
internal/provision/devcontainer/dotnet/verify.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package dotnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"zlh-agent/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dotnetBin = "/opt/zlh/runtimes/dotnet/current/dotnet"
|
||||||
|
|
||||||
|
func Verify(cfg state.Config) error {
|
||||||
|
if _, err := os.Stat(dotnetBin); err != nil {
|
||||||
|
return fmt.Errorf("dotnet binary missing at %s", dotnetBin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exec.Command(dotnetBin, "--info").Run(); err != nil {
|
||||||
|
return fmt.Errorf("dotnet runtime not executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -2,6 +2,9 @@ package goenv
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"zlh-agent/internal/provision/executil"
|
"zlh-agent/internal/provision/executil"
|
||||||
"zlh-agent/internal/provision/markers"
|
"zlh-agent/internal/provision/markers"
|
||||||
@ -9,8 +12,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Install(cfg state.Config) error {
|
func Install(cfg state.Config) error {
|
||||||
const marker = "devcontainer-go"
|
marker := runtimeMarker(cfg.Version)
|
||||||
|
if runtimeInstalled(cfg.Version) {
|
||||||
|
if !markers.IsPresent(marker) {
|
||||||
|
return markers.Write(marker)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if markers.IsPresent(marker) {
|
if markers.IsPresent(marker) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -26,3 +34,12 @@ func Install(cfg state.Config) error {
|
|||||||
|
|
||||||
return markers.Write(marker)
|
return markers.Write(marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runtimeInstalled(version string) bool {
|
||||||
|
info, err := os.Stat(filepath.Join("/opt/zlh/runtimes/go", strings.TrimSpace(version)))
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runtimeMarker(version string) string {
|
||||||
|
return "devcontainer-go-" + strings.ReplaceAll(strings.TrimSpace(version), "/", "_")
|
||||||
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"zlh-agent/internal/state"
|
"zlh-agent/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
const goBin = "/opt/zlh/runtime/go/bin/go"
|
const goBin = "/opt/zlh/runtimes/go/bin/go"
|
||||||
|
|
||||||
func Verify(cfg state.Config) error {
|
func Verify(cfg state.Config) error {
|
||||||
if _, err := os.Stat(goBin); err != nil {
|
if _, err := os.Stat(goBin); err != nil {
|
||||||
|
|||||||
@ -2,6 +2,9 @@ package java
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"zlh-agent/internal/provision/executil"
|
"zlh-agent/internal/provision/executil"
|
||||||
"zlh-agent/internal/provision/markers"
|
"zlh-agent/internal/provision/markers"
|
||||||
@ -9,8 +12,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Install(cfg state.Config) error {
|
func Install(cfg state.Config) error {
|
||||||
const marker = "devcontainer-java"
|
marker := runtimeMarker(cfg.Version)
|
||||||
|
if runtimeInstalled(cfg.Version) {
|
||||||
|
if !markers.IsPresent(marker) {
|
||||||
|
return markers.Write(marker)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if markers.IsPresent(marker) {
|
if markers.IsPresent(marker) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -26,3 +34,12 @@ func Install(cfg state.Config) error {
|
|||||||
|
|
||||||
return markers.Write(marker)
|
return markers.Write(marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runtimeInstalled(version string) bool {
|
||||||
|
info, err := os.Stat(filepath.Join("/opt/zlh/runtimes/java", strings.TrimSpace(version)))
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runtimeMarker(version string) string {
|
||||||
|
return "devcontainer-java-" + strings.ReplaceAll(strings.TrimSpace(version), "/", "_")
|
||||||
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"zlh-agent/internal/state"
|
"zlh-agent/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
const javaBin = "/opt/zlh/runtime/java/bin/java"
|
const javaBin = "/opt/zlh/runtimes/java/bin/java"
|
||||||
|
|
||||||
func Verify(cfg state.Config) error {
|
func Verify(cfg state.Config) error {
|
||||||
if _, err := os.Stat(javaBin); err != nil {
|
if _, err := os.Stat(javaBin); err != nil {
|
||||||
|
|||||||
@ -2,6 +2,9 @@ package node
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"zlh-agent/internal/provision/executil"
|
"zlh-agent/internal/provision/executil"
|
||||||
"zlh-agent/internal/provision/markers"
|
"zlh-agent/internal/provision/markers"
|
||||||
@ -9,8 +12,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Install(cfg state.Config) error {
|
func Install(cfg state.Config) error {
|
||||||
const marker = "devcontainer-node"
|
marker := runtimeMarker(cfg.Version)
|
||||||
|
if runtimeInstalled(cfg.Version) {
|
||||||
|
if !markers.IsPresent(marker) {
|
||||||
|
return markers.Write(marker)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if markers.IsPresent(marker) {
|
if markers.IsPresent(marker) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -26,3 +34,12 @@ func Install(cfg state.Config) error {
|
|||||||
|
|
||||||
return markers.Write(marker)
|
return markers.Write(marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runtimeInstalled(version string) bool {
|
||||||
|
info, err := os.Stat(filepath.Join("/opt/zlh/runtimes/node", strings.TrimSpace(version)))
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runtimeMarker(version string) string {
|
||||||
|
return "devcontainer-node-" + strings.ReplaceAll(strings.TrimSpace(version), "/", "_")
|
||||||
|
}
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
nodeBin = "/opt/zlh/runtime/node/bin/node"
|
nodeBin = "/opt/zlh/runtimes/node/bin/node"
|
||||||
npmBin = "/opt/zlh/runtime/node/bin/npm"
|
npmBin = "/opt/zlh/runtimes/node/bin/npm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Verify(cfg state.Config) error {
|
func Verify(cfg state.Config) error {
|
||||||
|
|||||||
@ -2,6 +2,9 @@ package python
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"zlh-agent/internal/provision/executil"
|
"zlh-agent/internal/provision/executil"
|
||||||
"zlh-agent/internal/provision/markers"
|
"zlh-agent/internal/provision/markers"
|
||||||
@ -9,8 +12,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Install(cfg state.Config) error {
|
func Install(cfg state.Config) error {
|
||||||
const marker = "devcontainer-python"
|
marker := runtimeMarker(cfg.Version)
|
||||||
|
if runtimeInstalled(cfg.Version) {
|
||||||
|
if !markers.IsPresent(marker) {
|
||||||
|
return markers.Write(marker)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if markers.IsPresent(marker) {
|
if markers.IsPresent(marker) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -26,3 +34,12 @@ func Install(cfg state.Config) error {
|
|||||||
|
|
||||||
return markers.Write(marker)
|
return markers.Write(marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runtimeInstalled(version string) bool {
|
||||||
|
info, err := os.Stat(filepath.Join("/opt/zlh/runtimes/python", strings.TrimSpace(version)))
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runtimeMarker(version string) string {
|
||||||
|
return "devcontainer-python-" + strings.ReplaceAll(strings.TrimSpace(version), "/", "_")
|
||||||
|
}
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
pythonBin = "/opt/zlh/runtime/python/bin/python3"
|
pythonBin = "/opt/zlh/runtimes/python/bin/python3"
|
||||||
pipBin = "/opt/zlh/runtime/python/bin/pip3"
|
pipBin = "/opt/zlh/runtimes/python/bin/pip3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Verify(cfg state.Config) error {
|
func Verify(cfg state.Config) error {
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"zlh-agent/internal/state"
|
|
||||||
"zlh-agent/internal/provision/addons"
|
"zlh-agent/internal/provision/addons"
|
||||||
"zlh-agent/internal/provision/devcontainer"
|
"zlh-agent/internal/provision/devcontainer"
|
||||||
"zlh-agent/internal/provision/minecraft"
|
"zlh-agent/internal/provision/minecraft"
|
||||||
"zlh-agent/internal/provision/steam"
|
"zlh-agent/internal/provision/steam"
|
||||||
|
"zlh-agent/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -133,6 +133,19 @@ func ProvisionAll(cfg state.Config) error {
|
|||||||
/* ---------------------------------------------------------
|
/* ---------------------------------------------------------
|
||||||
ADDONS (OPTIONAL, ROLE-AGNOSTIC)
|
ADDONS (OPTIONAL, ROLE-AGNOSTIC)
|
||||||
--------------------------------------------------------- */
|
--------------------------------------------------------- */
|
||||||
|
if cfg.ContainerType == "dev" && cfg.EnableCodeServer {
|
||||||
|
seen := false
|
||||||
|
for _, addon := range cfg.Addons {
|
||||||
|
if addon == "codeserver" {
|
||||||
|
seen = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !seen {
|
||||||
|
cfg.Addons = append(cfg.Addons, "codeserver")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(cfg.Addons) > 0 {
|
if len(cfg.Addons) > 0 {
|
||||||
if err := addons.Provision(cfg); err != nil {
|
if err := addons.Provision(cfg); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -22,13 +22,14 @@ type Config struct {
|
|||||||
|
|
||||||
// Dev runtime (only for dev containers)
|
// Dev runtime (only for dev containers)
|
||||||
Runtime string `json:"runtime,omitempty"`
|
Runtime string `json:"runtime,omitempty"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
// OPTIONAL addons (role-agnostic)
|
// OPTIONAL addons (role-agnostic)
|
||||||
Addons []string `json:"addons,omitempty"`
|
Addons []string `json:"addons,omitempty"`
|
||||||
|
EnableCodeServer bool `json:"enable_code_server,omitempty"`
|
||||||
|
|
||||||
Game string `json:"game"`
|
Game string `json:"game"`
|
||||||
Variant string `json:"variant"`
|
Variant string `json:"variant"`
|
||||||
Version string `json:"version"`
|
|
||||||
World string `json:"world"`
|
World string `json:"world"`
|
||||||
Ports []int `json:"ports"`
|
Ports []int `json:"ports"`
|
||||||
ArtifactPath string `json:"artifact_path"`
|
ArtifactPath string `json:"artifact_path"`
|
||||||
@ -72,6 +73,7 @@ type agentStatus struct {
|
|||||||
lastError error
|
lastError error
|
||||||
crashCount int
|
crashCount int
|
||||||
lastCrash time.Time
|
lastCrash time.Time
|
||||||
|
lastCrashInfo *CrashInfo
|
||||||
intentionalStop bool
|
intentionalStop bool
|
||||||
ready bool
|
ready bool
|
||||||
readySource string
|
readySource string
|
||||||
@ -79,11 +81,27 @@ type agentStatus struct {
|
|||||||
lastReadyAt time.Time
|
lastReadyAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CrashInfo struct {
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
ExitCode int `json:"exitCode"`
|
||||||
|
Signal int `json:"signal"`
|
||||||
|
UptimeSeconds int64 `json:"uptimeSeconds"`
|
||||||
|
LogTail []string `json:"logTail"`
|
||||||
|
}
|
||||||
|
|
||||||
var global = &agentStatus{
|
var global = &agentStatus{
|
||||||
state: StateIdle,
|
state: StateIdle,
|
||||||
lastChange: time.Now(),
|
lastChange: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stateLogf(format string, args ...any) {
|
||||||
|
if cfg, err := LoadConfig(); err == nil && cfg != nil {
|
||||||
|
log.Printf("[state] vmid=%d "+format, append([]any{cfg.VMID}, args...)...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[state] "+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------------------------
|
/* --------------------------------------------------------------------------
|
||||||
STATE GETTERS
|
STATE GETTERS
|
||||||
----------------------------------------------------------------------------*/
|
----------------------------------------------------------------------------*/
|
||||||
@ -142,6 +160,21 @@ func GetLastReadyAt() time.Time {
|
|||||||
return global.lastReadyAt
|
return global.lastReadyAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetLastCrash() *CrashInfo {
|
||||||
|
global.mu.Lock()
|
||||||
|
defer global.mu.Unlock()
|
||||||
|
|
||||||
|
if global.lastCrashInfo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
copyInfo := *global.lastCrashInfo
|
||||||
|
if global.lastCrashInfo.LogTail != nil {
|
||||||
|
copyInfo.LogTail = append([]string(nil), global.lastCrashInfo.LogTail...)
|
||||||
|
}
|
||||||
|
return ©Info
|
||||||
|
}
|
||||||
|
|
||||||
func IsIntentionalStop() bool {
|
func IsIntentionalStop() bool {
|
||||||
global.mu.Lock()
|
global.mu.Lock()
|
||||||
defer global.mu.Unlock()
|
defer global.mu.Unlock()
|
||||||
@ -157,7 +190,7 @@ func SetState(s AgentState) {
|
|||||||
defer global.mu.Unlock()
|
defer global.mu.Unlock()
|
||||||
|
|
||||||
if global.state != s {
|
if global.state != s {
|
||||||
log.Printf("[state] %s → %s\n", global.state, s)
|
stateLogf("%s -> %s", global.state, s)
|
||||||
global.state = s
|
global.state = s
|
||||||
global.lastChange = time.Now()
|
global.lastChange = time.Now()
|
||||||
}
|
}
|
||||||
@ -187,11 +220,27 @@ func ClearIntentionalStop() {
|
|||||||
global.intentionalStop = false
|
global.intentionalStop = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetLastCrash(info *CrashInfo) {
|
||||||
|
global.mu.Lock()
|
||||||
|
defer global.mu.Unlock()
|
||||||
|
|
||||||
|
if info == nil {
|
||||||
|
global.lastCrashInfo = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
copyInfo := *info
|
||||||
|
if info.LogTail != nil {
|
||||||
|
copyInfo.LogTail = append([]string(nil), info.LogTail...)
|
||||||
|
}
|
||||||
|
global.lastCrashInfo = ©Info
|
||||||
|
}
|
||||||
|
|
||||||
func RecordCrash(err error) {
|
func RecordCrash(err error) {
|
||||||
global.mu.Lock()
|
global.mu.Lock()
|
||||||
defer global.mu.Unlock()
|
defer global.mu.Unlock()
|
||||||
|
|
||||||
log.Printf("[state] crash detected: %v", err)
|
stateLogf("crash recorded err=%v", err)
|
||||||
|
|
||||||
global.state = StateCrashed
|
global.state = StateCrashed
|
||||||
global.lastError = err
|
global.lastError = err
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"zlh-agent/internal/provision"
|
"zlh-agent/internal/provision"
|
||||||
|
"zlh-agent/internal/provision/devcontainer"
|
||||||
"zlh-agent/internal/runtime"
|
"zlh-agent/internal/runtime"
|
||||||
"zlh-agent/internal/state"
|
"zlh-agent/internal/state"
|
||||||
)
|
)
|
||||||
@ -22,6 +29,7 @@ var (
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
serverCmd *exec.Cmd
|
serverCmd *exec.Cmd
|
||||||
serverPTY *os.File
|
serverPTY *os.File
|
||||||
|
serverStartTime time.Time
|
||||||
|
|
||||||
devCmd *exec.Cmd
|
devCmd *exec.Cmd
|
||||||
devPTY *os.File
|
devPTY *os.File
|
||||||
@ -52,6 +60,7 @@ func StartServer(cfg *state.Config) error {
|
|||||||
|
|
||||||
dir := provision.ServerDir(*cfg)
|
dir := provision.ServerDir(*cfg)
|
||||||
startScript := filepath.Join(dir, "start.sh")
|
startScript := filepath.Join(dir, "start.sh")
|
||||||
|
log.Printf("[process] vmid=%d server start requested dir=%s", cfg.VMID, dir)
|
||||||
|
|
||||||
cmd := exec.Command("/bin/bash", startScript)
|
cmd := exec.Command("/bin/bash", startScript)
|
||||||
cmd.Dir = dir
|
cmd.Dir = dir
|
||||||
@ -63,11 +72,13 @@ func StartServer(cfg *state.Config) error {
|
|||||||
|
|
||||||
serverCmd = cmd
|
serverCmd = cmd
|
||||||
serverPTY = ptmx
|
serverPTY = ptmx
|
||||||
|
serverStartTime = time.Now()
|
||||||
|
|
||||||
state.ClearIntentionalStop()
|
state.ClearIntentionalStop()
|
||||||
state.SetState(state.StateRunning)
|
state.SetState(state.StateRunning)
|
||||||
state.SetError(nil)
|
state.SetError(nil)
|
||||||
state.SetReadyState(false, "", "")
|
state.SetReadyState(false, "", "")
|
||||||
|
log.Printf("[process] vmid=%d server process started", cfg.VMID)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := cmd.Wait()
|
err := cmd.Wait()
|
||||||
@ -83,15 +94,27 @@ func StartServer(cfg *state.Config) error {
|
|||||||
state.ClearIntentionalStop()
|
state.ClearIntentionalStop()
|
||||||
state.SetState(state.StateIdle)
|
state.SetState(state.StateIdle)
|
||||||
state.SetReadyState(false, "", "")
|
state.SetReadyState(false, "", "")
|
||||||
|
log.Printf("[process] vmid=%d server exited after intentional stop", cfg.VMID)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
crashInfo := buildCrashInfo(cfg, err, serverStartTime)
|
||||||
|
state.SetLastCrash(crashInfo)
|
||||||
|
log.Printf("[process] server crashed vmid=%d exit_code=%d signal=%d uptime=%ds", cfg.VMID, crashInfo.ExitCode, crashInfo.Signal, crashInfo.UptimeSeconds)
|
||||||
|
if len(crashInfo.LogTail) > 0 {
|
||||||
|
log.Printf("[process] crash log tail:")
|
||||||
|
for _, line := range lastLines(crashInfo.LogTail, 20) {
|
||||||
|
log.Printf("[process] %s", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
state.RecordCrash(err)
|
state.RecordCrash(err)
|
||||||
} else {
|
} else {
|
||||||
state.SetState(state.StateIdle)
|
state.SetState(state.StateIdle)
|
||||||
state.SetReadyState(false, "", "")
|
state.SetReadyState(false, "", "")
|
||||||
|
log.Printf("[process] vmid=%d server exited cleanly", cfg.VMID)
|
||||||
}
|
}
|
||||||
|
|
||||||
serverCmd = nil
|
serverCmd = nil
|
||||||
serverPTY = nil
|
serverPTY = nil
|
||||||
|
serverStartTime = time.Time{}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -109,6 +132,12 @@ func StopServer() error {
|
|||||||
return fmt.Errorf("server not running")
|
return fmt.Errorf("server not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg, _ := state.LoadConfig()
|
||||||
|
if cfg != nil {
|
||||||
|
log.Printf("[process] vmid=%d stop requested", cfg.VMID)
|
||||||
|
} else {
|
||||||
|
log.Printf("[process] stop requested")
|
||||||
|
}
|
||||||
state.SetState(state.StateStopping)
|
state.SetState(state.StateStopping)
|
||||||
state.MarkIntentionalStop()
|
state.MarkIntentionalStop()
|
||||||
state.SetReadyState(false, "", "")
|
state.SetReadyState(false, "", "")
|
||||||
@ -186,6 +215,9 @@ func StartDevShell() (*os.File, error) {
|
|||||||
if devPTY != nil && devCmd != nil {
|
if devPTY != nil && devCmd != nil {
|
||||||
return devPTY, nil
|
return devPTY, nil
|
||||||
}
|
}
|
||||||
|
if err := devcontainer.EnsureDevUserEnvironment(); err != nil {
|
||||||
|
return nil, fmt.Errorf("prepare dev environment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
shell := "/bin/bash"
|
shell := "/bin/bash"
|
||||||
if _, err := os.Stat(shell); err != nil {
|
if _, err := os.Stat(shell); err != nil {
|
||||||
@ -198,7 +230,32 @@ func StartDevShell() (*os.File, error) {
|
|||||||
} else {
|
} else {
|
||||||
cmd = exec.Command(shell, "-i")
|
cmd = exec.Command(shell, "-i")
|
||||||
}
|
}
|
||||||
cmd.Dir = "/opt"
|
cmd.Dir = devcontainer.WorkspaceDir
|
||||||
|
|
||||||
|
devUser, err := user.Lookup(devcontainer.DevUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("lookup dev user: %w", err)
|
||||||
|
}
|
||||||
|
uid, err := strconv.Atoi(devUser.Uid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse dev uid: %w", err)
|
||||||
|
}
|
||||||
|
gid, err := strconv.Atoi(devUser.Gid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse dev gid: %w", err)
|
||||||
|
}
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
"HOME="+devcontainer.DevHome,
|
||||||
|
"USER="+devcontainer.DevUser,
|
||||||
|
"LOGNAME="+devcontainer.DevUser,
|
||||||
|
"TERM=xterm-256color",
|
||||||
|
)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Credential: &syscall.Credential{
|
||||||
|
Uid: uint32(uid),
|
||||||
|
Gid: uint32(gid),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
ptmx, err := runtime.CreatePTY(cmd)
|
ptmx, err := runtime.CreatePTY(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -302,3 +359,63 @@ func StopDevShell() error {
|
|||||||
state.SetState(state.StateIdle)
|
state.SetState(state.StateIdle)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildCrashInfo(cfg *state.Config, waitErr error, startedAt time.Time) *state.CrashInfo {
|
||||||
|
exitCode, signal := extractExitDetails(waitErr)
|
||||||
|
uptime := int64(0)
|
||||||
|
if !startedAt.IsZero() {
|
||||||
|
uptime = int64(time.Since(startedAt).Seconds())
|
||||||
|
if uptime < 0 {
|
||||||
|
uptime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &state.CrashInfo{
|
||||||
|
Time: time.Now().UTC(),
|
||||||
|
ExitCode: exitCode,
|
||||||
|
Signal: signal,
|
||||||
|
UptimeSeconds: uptime,
|
||||||
|
LogTail: tailLogLines(cfg, 40),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractExitDetails(err error) (int, int) {
|
||||||
|
exitCode := -1
|
||||||
|
signal := 0
|
||||||
|
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if !errors.As(err, &exitErr) {
|
||||||
|
return exitCode, signal
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCode = exitErr.ExitCode()
|
||||||
|
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok && status.Signaled() {
|
||||||
|
signal = int(status.Signal())
|
||||||
|
}
|
||||||
|
return exitCode, signal
|
||||||
|
}
|
||||||
|
|
||||||
|
func tailLogLines(cfg *state.Config, maxLines int) []string {
|
||||||
|
buf, err := TailLatestLog(cfg, 64*1024)
|
||||||
|
if err != nil || len(buf) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawLines := bytes.Split(buf, []byte{'\n'})
|
||||||
|
lines := make([]string, 0, len(rawLines))
|
||||||
|
for _, raw := range rawLines {
|
||||||
|
line := strings.TrimSpace(string(raw))
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
return lastLines(lines, maxLines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lastLines(lines []string, maxLines int) []string {
|
||||||
|
if len(lines) <= maxLines {
|
||||||
|
return append([]string(nil), lines...)
|
||||||
|
}
|
||||||
|
return append([]string(nil), lines[len(lines)-maxLines:]...)
|
||||||
|
}
|
||||||
|
|||||||
@ -6,55 +6,79 @@ echo "[code-server] starting install"
|
|||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Config
|
# Config
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
ADDON_ROOT="/opt/zlh/addons/code-server"
|
SERVICE_ROOT="/opt/zlh/services/code-server"
|
||||||
ARTIFACT_DIR="/opt/zlh/addons/code-server"
|
ZLH_ARTIFACT_BASE_URL="${ZLH_ARTIFACT_BASE_URL:-http://10.60.0.251:8080}"
|
||||||
|
ARTIFACT_URL="${ZLH_ARTIFACT_BASE_URL%/}/addons/code-server/code-server.zip"
|
||||||
|
ARTIFACT_TMP="/tmp/code-server.zip"
|
||||||
MARKER="/opt/zlh/.zlh/addons/code-server.installed"
|
MARKER="/opt/zlh/.zlh/addons/code-server.installed"
|
||||||
|
PID_FILE="/opt/zlh/.zlh/addons/code-server.pid"
|
||||||
|
LOG_FILE="/opt/zlh/logs/code-server.log"
|
||||||
|
WORKSPACE_DIR="/home/dev/workspace"
|
||||||
|
BIN="${SERVICE_ROOT}/bin/code-server"
|
||||||
|
LINK_PATH="/usr/local/bin/code-server"
|
||||||
|
|
||||||
mkdir -p "$(dirname "${MARKER}")"
|
mkdir -p "$(dirname "${MARKER}")"
|
||||||
|
mkdir -p "$(dirname "${LOG_FILE}")"
|
||||||
|
|
||||||
# --------------------------------------------------
|
download_artifact() {
|
||||||
# Idempotency
|
echo "[code-server] downloading ${ARTIFACT_URL}"
|
||||||
# --------------------------------------------------
|
if command -v curl >/dev/null 2>&1; then
|
||||||
if [ -f "${MARKER}" ]; then
|
curl -fL "${ARTIFACT_URL}" -o "${ARTIFACT_TMP}"
|
||||||
echo "[code-server] already installed"
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
exit 0
|
wget -O "${ARTIFACT_TMP}" "${ARTIFACT_URL}"
|
||||||
fi
|
else
|
||||||
|
echo "[code-server][ERROR] curl or wget is required"
|
||||||
ARCHIVE="$(ls ${ARTIFACT_DIR}/code-server.* 2>/dev/null | head -n1)"
|
|
||||||
if [ -z "${ARCHIVE}" ]; then
|
|
||||||
echo "[code-server][ERROR] artifact not found"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
echo "[code-server] extracting ${ARCHIVE}"
|
extract_artifact() {
|
||||||
mkdir -p "${ADDON_ROOT}"
|
local tmp_dir
|
||||||
|
tmp_dir="$(mktemp -d)"
|
||||||
|
|
||||||
TMP_DIR="$(mktemp -d)"
|
if command -v unzip >/dev/null 2>&1; then
|
||||||
|
unzip -q "${ARTIFACT_TMP}" -d "${tmp_dir}"
|
||||||
|
elif command -v bsdtar >/dev/null 2>&1; then
|
||||||
|
bsdtar -xf "${ARTIFACT_TMP}" -C "${tmp_dir}"
|
||||||
|
else
|
||||||
|
echo "[code-server][ERROR] unzip or bsdtar is required"
|
||||||
|
exit 127
|
||||||
|
fi
|
||||||
|
|
||||||
case "${ARCHIVE}" in
|
EXTRACTED_DIR="$(find "${tmp_dir}" -maxdepth 1 -type d -name 'code-server*' | head -n1)"
|
||||||
*.tar.gz)
|
|
||||||
tar -xzf "${ARCHIVE}" -C "${TMP_DIR}"
|
|
||||||
;;
|
|
||||||
*.zip)
|
|
||||||
unzip -q "${ARCHIVE}" -d "${TMP_DIR}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "[code-server][ERROR] unsupported archive format"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
EXTRACTED_DIR="$(find ${TMP_DIR} -maxdepth 1 -type d -name 'code-server*' | head -n1)"
|
|
||||||
if [ -z "${EXTRACTED_DIR}" ]; then
|
if [ -z "${EXTRACTED_DIR}" ]; then
|
||||||
echo "[code-server][ERROR] failed to locate extracted directory"
|
echo "[code-server][ERROR] failed to locate extracted directory"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mv "${EXTRACTED_DIR}"/* "${ADDON_ROOT}/"
|
mv "${EXTRACTED_DIR}"/* "${SERVICE_ROOT}/"
|
||||||
rm -rf "${TMP_DIR}"
|
rm -rf "${tmp_dir}"
|
||||||
|
}
|
||||||
|
|
||||||
chmod +x "${ADDON_ROOT}/bin/code-server"
|
# --------------------------------------------------
|
||||||
|
# Idempotency
|
||||||
|
# --------------------------------------------------
|
||||||
|
if [ ! -x "${BIN}" ]; then
|
||||||
|
download_artifact
|
||||||
|
echo "[code-server] extracting ${ARTIFACT_TMP}"
|
||||||
|
rm -rf "${SERVICE_ROOT}"
|
||||||
|
mkdir -p "${SERVICE_ROOT}"
|
||||||
|
extract_artifact
|
||||||
|
chmod +x "${BIN}"
|
||||||
|
ln -sfn "${BIN}" "${LINK_PATH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${WORKSPACE_DIR}"
|
||||||
|
|
||||||
|
if [ -f "${PID_FILE}" ] && kill -0 "$(cat "${PID_FILE}")" 2>/dev/null; then
|
||||||
|
echo "[code-server] already running"
|
||||||
|
else
|
||||||
|
rm -f "${PID_FILE}"
|
||||||
|
nohup "${BIN}" --bind-addr 0.0.0.0:8080 "${WORKSPACE_DIR}" >"${LOG_FILE}" 2>&1 &
|
||||||
|
echo $! > "${PID_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
touch "${MARKER}"
|
touch "${MARKER}"
|
||||||
|
rm -f "${ARTIFACT_TMP}"
|
||||||
|
|
||||||
echo "[code-server] install complete"
|
echo "[code-server] install complete"
|
||||||
|
|||||||
54
scripts/devcontainer/dotnet/install.sh
Normal file
54
scripts/devcontainer/dotnet/install.sh
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${RUNTIME_VERSION:?RUNTIME_VERSION required}"
|
||||||
|
|
||||||
|
RUNTIME="dotnet"
|
||||||
|
RUNTIME_ROOT="/opt/zlh/runtimes/${RUNTIME}"
|
||||||
|
DEST_DIR="${RUNTIME_ROOT}/${RUNTIME_VERSION}"
|
||||||
|
CURRENT_LINK="${RUNTIME_ROOT}/current"
|
||||||
|
ZLH_ARTIFACT_BASE_URL="${ZLH_ARTIFACT_BASE_URL:-http://10.60.0.251:8080}"
|
||||||
|
INSTALLER_URL="${ZLH_ARTIFACT_BASE_URL%/}/devcontainer/dotnet/dotnet-install.sh"
|
||||||
|
INSTALLER_TMP="/tmp/dotnet-install.sh"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[zlh-installer:${RUNTIME}] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "[zlh-installer:${RUNTIME}] ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
download_installer() {
|
||||||
|
log "Downloading ${INSTALLER_URL}"
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fL "${INSTALLER_URL}" -o "${INSTALLER_TMP}"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -O "${INSTALLER_TMP}" "${INSTALLER_URL}"
|
||||||
|
else
|
||||||
|
fail "curl or wget is required"
|
||||||
|
fi
|
||||||
|
chmod +x "${INSTALLER_TMP}"
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "${RUNTIME_ROOT}"
|
||||||
|
|
||||||
|
if [[ -d "${DEST_DIR}" ]]; then
|
||||||
|
log "Version already installed at ${DEST_DIR}"
|
||||||
|
else
|
||||||
|
mkdir -p "${DEST_DIR}"
|
||||||
|
download_installer
|
||||||
|
log "Installing dotnet ${RUNTIME_VERSION} into ${DEST_DIR}"
|
||||||
|
bash "${INSTALLER_TMP}" --channel "${RUNTIME_VERSION}" --install-dir "${DEST_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ln -sfn "${DEST_DIR}" "${CURRENT_LINK}"
|
||||||
|
cat >/etc/profile.d/zlh-dotnet.sh <<EOF
|
||||||
|
export PATH="${CURRENT_LINK}:\$PATH"
|
||||||
|
EOF
|
||||||
|
chmod +x /etc/profile.d/zlh-dotnet.sh
|
||||||
|
chmod -R 755 "${DEST_DIR}"
|
||||||
|
rm -f "${INSTALLER_TMP}"
|
||||||
|
|
||||||
|
log "Install complete"
|
||||||
@ -14,7 +14,7 @@ set -euo pipefail
|
|||||||
############################################
|
############################################
|
||||||
|
|
||||||
ZLH_ARTIFACT_BASE_URL="${ZLH_ARTIFACT_BASE_URL:-http://10.60.0.251:8080}"
|
ZLH_ARTIFACT_BASE_URL="${ZLH_ARTIFACT_BASE_URL:-http://10.60.0.251:8080}"
|
||||||
ZLH_RUNTIME_ROOT="${ZLH_RUNTIME_ROOT:-/opt/zlh/runtime}"
|
ZLH_RUNTIME_ROOT="${ZLH_RUNTIME_ROOT:-/opt/zlh/runtimes}"
|
||||||
ARCHIVE_PREFIX="${ARCHIVE_PREFIX:-${RUNTIME}}"
|
ARCHIVE_PREFIX="${ARCHIVE_PREFIX:-${RUNTIME}}"
|
||||||
|
|
||||||
############################################
|
############################################
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user