fix(controller): fixes for a remote update
This commit is contained in:
4
.include
4
.include
@@ -11,5 +11,7 @@ package.json
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
lstV2
|
lstV2
|
||||||
lang
|
lang
|
||||||
|
scripts/services.ps1
|
||||||
drizzle.config.ts
|
drizzle.config.ts
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
|
.includeCleanup
|
||||||
|
|||||||
7
.includeCleanup
Normal file
7
.includeCleanup
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
dist
|
||||||
|
tmp/
|
||||||
|
frontend/dist
|
||||||
|
lstDocs/build
|
||||||
|
oldbuild.log
|
||||||
|
lstV2/dist
|
||||||
|
lstV2/frontend/dist
|
||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
socketio "github.com/googollee/go-socket.io"
|
socketio "github.com/googollee/go-socket.io"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
router "lst.net/internal/route_handler"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
err := godotenv.Load("../.env")
|
err := godotenv.Load("../.env")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Warning: .env file not found")
|
fmt.Println("Warning: .env file not found")
|
||||||
@@ -29,10 +29,11 @@ func main() {
|
|||||||
if os.Getenv("NODE_ENV") != "production" {
|
if os.Getenv("NODE_ENV") != "production" {
|
||||||
basePath = "/lst/api/controller"
|
basePath = "/lst/api/controller"
|
||||||
}
|
}
|
||||||
r := router.Setup(basePath) // returns *gin.Engine
|
|
||||||
|
|
||||||
server := socketio.NewServer(nil)
|
server := socketio.NewServer(nil)
|
||||||
|
|
||||||
|
r := Setup(basePath, server) // returns *gin.Engine
|
||||||
|
|
||||||
server.OnConnect("/", func(s socketio.Conn) error {
|
server.OnConnect("/", func(s socketio.Conn) error {
|
||||||
fmt.Println("✅ Client connected:", s.ID())
|
fmt.Println("✅ Client connected:", s.ID())
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
package router
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
socketio "github.com/googollee/go-socket.io"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UpdatePayload struct {
|
func Setup(basePath string, server *socketio.Server) *gin.Engine {
|
||||||
Action string `json:"action"`
|
|
||||||
Target string `json:"target"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Setup(basePath string) *gin.Engine {
|
|
||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
@@ -44,12 +39,18 @@ func Setup(basePath string) *gin.Engine {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
steps := []string{"🚀 Starting...", "📂 Copying...", "🔧 Migrating...", "✅ Done!"}
|
// Start the update and get the channel
|
||||||
for _, step := range steps {
|
updates := UpdateApp(server)
|
||||||
fmt.Fprintf(c.Writer, "event: log\ndata: %s\n\n", step)
|
|
||||||
flusher.Flush() // 🔑 actually push chunk
|
for msg := range updates {
|
||||||
time.Sleep(1 * time.Second) // simulate work
|
//fmt.Fprintf(c.Writer, "event: log\ndata: %s\n\n", msg)
|
||||||
|
fmt.Fprintf(c.Writer, "%s\n\n", msg)
|
||||||
|
flusher.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(c.Writer, "Update process finished\n\n")
|
||||||
|
flusher.Flush()
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Any(basePath+"/", func(c *gin.Context) { errorApiLoc(c) })
|
r.Any(basePath+"/", func(c *gin.Context) { errorApiLoc(c) })
|
||||||
305
controller/update.go
Normal file
305
controller/update.go
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
socketio "github.com/googollee/go-socket.io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateApp(server *socketio.Server) <-chan string {
|
||||||
|
updates := make(chan string)
|
||||||
|
rootDir := filepath.Join("..")
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(rootDir)
|
||||||
|
if err != nil {
|
||||||
|
//log.Fatal("failed to read root dir: %v", err)
|
||||||
|
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", fmt.Sprintf("failed to read root dir: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var zips []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() && filepath.Ext(e.Name()) == ".zip" {
|
||||||
|
zips = append(zips, filepath.Join(rootDir, e.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(updates)
|
||||||
|
|
||||||
|
switch len(zips) {
|
||||||
|
case 0:
|
||||||
|
msg := "No zip files found in root directory"
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
case 1:
|
||||||
|
version, err := extractVersion(filepath.Base(zips[0]))
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("could not parse version: %v", err)
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
}
|
||||||
|
msg := "Stopping the services"
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
// 1. Stop services + pool
|
||||||
|
stopService("LSTV2")
|
||||||
|
stopService("lst_app")
|
||||||
|
stopAppPool("LogisticsSupportTool")
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
msg = "Checking cleanup of files"
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
|
||||||
|
if version%10 == 0 {
|
||||||
|
msg = fmt.Sprintf("Release %d is a cleanup milestone — cleaning root", version)
|
||||||
|
updates <- msg
|
||||||
|
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
if err := cleanupRoot(rootDir); err != nil {
|
||||||
|
msg = fmt.Sprintf("cleanup failed: %v", err)
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = "Unzipping New Build"
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
if err := Unzip(zips[0], rootDir); err != nil {
|
||||||
|
msg = fmt.Sprintf("unzip failed: %v", err)
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = "Running App Update"
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
if err := runNPMInstall(rootDir); err != nil {
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", fmt.Sprintf("npm install failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = "Running DB Migrations"
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
if err := runNPMMigrate(rootDir); err != nil {
|
||||||
|
msg = fmt.Sprintf("npm migrate failed: %v", err)
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = "Starting Services back up"
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
startService("lst_app")
|
||||||
|
startService("LSTV2")
|
||||||
|
startAppPool("LogisticsSupportTool")
|
||||||
|
|
||||||
|
if err := os.Remove(zips[0]); err != nil {
|
||||||
|
msg = fmt.Sprintf("warning: failed to delete zip %s: %v", zips[0], err)
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
} else {
|
||||||
|
msg = fmt.Sprintf("Deleted zip %s", zips[0])
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = "Deployment finished successfully"
|
||||||
|
updates <- msg
|
||||||
|
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
default:
|
||||||
|
msg := fmt.Sprintf("Error: too many zip files in root: %v\n", zips)
|
||||||
|
updates <- msg
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return updates
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unzip(srcZip, destDir string) error {
|
||||||
|
|
||||||
|
r, err := zip.OpenReader(srcZip)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening zip: %w", err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
for _, f := range r.File {
|
||||||
|
// Build the destination path for each file
|
||||||
|
fpath := filepath.Join(destDir, f.Name)
|
||||||
|
|
||||||
|
// Check for ZipSlip (directory traversal)
|
||||||
|
if !strings.HasPrefix(fpath, filepath.Clean(destDir)+string(os.PathSeparator)) {
|
||||||
|
return fmt.Errorf("illegal file path: %s", fpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.FileInfo().IsDir() {
|
||||||
|
// Create folder
|
||||||
|
if err := os.MkdirAll(fpath, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure target folder exists
|
||||||
|
if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open file in archive
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create destination file
|
||||||
|
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||||
|
if err != nil {
|
||||||
|
rc.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy file contents
|
||||||
|
_, err = io.Copy(outFile, rc)
|
||||||
|
|
||||||
|
// Close files
|
||||||
|
outFile.Close()
|
||||||
|
rc.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopService runs "sc stop <service>"
|
||||||
|
func stopService(name string) error {
|
||||||
|
cmd := exec.Command("sc", "stop", name)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("Stopped service %s: %s", name, output)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopAppPool runs appcmd to stop an IIS app pool
|
||||||
|
func stopAppPool(name string) error {
|
||||||
|
cmd := exec.Command(`C:\Windows\System32\inetsrv\appcmd.exe`, "stop", "apppool", "/apppool.name:"+name)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stop app pool %s: %v, output: %s", name, err, string(output))
|
||||||
|
}
|
||||||
|
log.Printf("Stopped app pool %s: %s", name, string(output))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupRoot(rootDir string) error {
|
||||||
|
// Default cleanup targets
|
||||||
|
targets := []string{
|
||||||
|
"dist",
|
||||||
|
filepath.Join("frontend", "dist"),
|
||||||
|
filepath.Join("lstDocs", "build"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if .includeCleanup file exists
|
||||||
|
cleanupFile := filepath.Join(rootDir, ".includeCleanup")
|
||||||
|
if _, err := os.Stat(cleanupFile); err == nil {
|
||||||
|
f, err := os.Open(cleanupFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
sc := bufio.NewScanner(f)
|
||||||
|
for sc.Scan() {
|
||||||
|
line := strings.TrimSpace(sc.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targets = append(targets, line)
|
||||||
|
}
|
||||||
|
if sc.Err() != nil {
|
||||||
|
return sc.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete them (remove dirs/files under rootDir)
|
||||||
|
for _, t := range targets {
|
||||||
|
p := filepath.Join(rootDir, t)
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
log.Printf("Removing %s", p)
|
||||||
|
if err := os.RemoveAll(p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractVersion(filename string) (int, error) {
|
||||||
|
re := regexp.MustCompile(`release-(\d+)\.zip`)
|
||||||
|
m := re.FindStringSubmatch(filename)
|
||||||
|
if m == nil {
|
||||||
|
return 0, fmt.Errorf("filename does not match release pattern: %s", filename)
|
||||||
|
}
|
||||||
|
return strconv.Atoi(m[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNPMInstall(rootDir string) error {
|
||||||
|
frontendDir := filepath.Join(rootDir, "frontend") // adapt if needed
|
||||||
|
cmd := exec.Command("npm", "install")
|
||||||
|
cmd.Dir = frontendDir
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
log.Println("Running npm install in", frontendDir)
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNPMMigrate(rootDir string) error {
|
||||||
|
frontendDir := filepath.Join(rootDir, "frontend") // same dir
|
||||||
|
cmd := exec.Command("npm", "run", "db:migrate")
|
||||||
|
cmd.Dir = frontendDir
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
log.Println("Running npm run db:migrate in", frontendDir)
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startService(name string) error {
|
||||||
|
cmd := exec.Command("sc", "start", name)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("Started service %s: %s", name, output)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startAppPool(name string) error {
|
||||||
|
cmd := exec.Command(`C:\Windows\System32\inetsrv\appcmd.exe`, "start", "apppool", "/apppool.name:"+name)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stop app pool %s: %v, output: %s", name, err, string(output))
|
||||||
|
}
|
||||||
|
log.Printf("Stopped app pool %s: %s", name, string(output))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
socketio "github.com/googollee/go-socket.io"
|
socketio "github.com/googollee/go-socket.io"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,8 +32,6 @@ func registerUpdateChannel(server *socketio.Server) {
|
|||||||
server.OnEvent("/", "update", func(s socketio.Conn, payload UpdatePayload) {
|
server.OnEvent("/", "update", func(s socketio.Conn, payload UpdatePayload) {
|
||||||
switch strings.ToLower(payload.Action) {
|
switch strings.ToLower(payload.Action) {
|
||||||
case "copy":
|
case "copy":
|
||||||
server.BroadcastToRoom("/", "update", "updateLogs",
|
|
||||||
fmt.Sprintf("🚀 Copying latest build to %v", payload.Target))
|
|
||||||
copyLatestBuild(server, payload.Target)
|
copyLatestBuild(server, payload.Target)
|
||||||
|
|
||||||
case "update":
|
case "update":
|
||||||
@@ -59,6 +58,7 @@ func updateServer(server *socketio.Server, target string) {
|
|||||||
|
|
||||||
if strings.Contains(host, "VMS") || strings.Contains(host, "vms") {
|
if strings.Contains(host, "VMS") || strings.Contains(host, "vms") {
|
||||||
server.BroadcastToRoom("/", "update", "updateLogs", "Your are about to check for a new build and then update the server.")
|
server.BroadcastToRoom("/", "update", "updateLogs", "Your are about to check for a new build and then update the server.")
|
||||||
|
go UpdateApp(server)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -80,16 +80,16 @@ func copyLatestBuild(server *socketio.Server, target string) {
|
|||||||
|
|
||||||
func triggerRemoteUpdate(server *socketio.Server, remoteURL string, payload UpdatePayload) {
|
func triggerRemoteUpdate(server *socketio.Server, remoteURL string, payload UpdatePayload) {
|
||||||
|
|
||||||
basePath := "/api/controller"
|
// basePath := "/api/controller"
|
||||||
if os.Getenv("NODE_ENV") != "production" {
|
// if os.Getenv("NODE_ENV") != "production" {
|
||||||
basePath = "/lst/api/controller"
|
// basePath = "/lst/api/controller"
|
||||||
}
|
// }
|
||||||
|
|
||||||
// send POST request with JSON, expect SSE / streaming text back
|
// send POST request with JSON, expect SSE / streaming text back
|
||||||
body, _ := json.Marshal(payload)
|
body, _ := json.Marshal(payload)
|
||||||
|
|
||||||
url := fmt.Sprintf("https://%v.alpla.net%v/update", remoteURL, basePath)
|
//url := fmt.Sprintf("https://%v.alpla.net%v/update", remoteURL, basePath)
|
||||||
//url := fmt.Sprintf("http://localhost:8080%v/update", basePath)
|
url := fmt.Sprintf("http://%v:8080/api/controller/update", remoteURL)
|
||||||
fmt.Println(url)
|
fmt.Println(url)
|
||||||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
|
resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,3 +116,16 @@ func triggerRemoteUpdate(server *socketio.Server, remoteURL string, payload Upda
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendUpdate(c *gin.Context, flusher http.Flusher, server *socketio.Server, msg string) {
|
||||||
|
// SSE stream
|
||||||
|
if c != nil && flusher != nil {
|
||||||
|
fmt.Fprintf(c.Writer, "event: log\ndata: %s\n\n", msg)
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socket.IO
|
||||||
|
if server != nil {
|
||||||
|
server.BroadcastToRoom("/", "update", "updateLogs", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user