Compare commits

...

4 Commits

9 changed files with 399 additions and 71 deletions

View File

@@ -11,5 +11,7 @@ package.json
package-lock.json
lstV2
lang
scripts/services.ps1
drizzle.config.ts
tsconfig.json
tsconfig.json
.includeCleanup

7
.includeCleanup Normal file
View File

@@ -0,0 +1,7 @@
dist
tmp/
frontend/dist
lstDocs/build
oldbuild.log
lstV2/dist
lstV2/frontend/dist

View File

@@ -69,23 +69,18 @@ const main = async () => {
app.use(
basePath + "/test",
express.static(join(__dirname, "../../controller"))
express.static(join(__dirname, "../controller"))
);
}
// docs and api stuff
app.use(
basePath + "/d",
express.static(join(__dirname, "../../lstDocs/build"))
express.static(join(__dirname, "../lstDocs/build"))
);
app.use(
basePath + "/app",
express.static(join(__dirname, "../../frontend/dist"))
);
app.use(
basePath + "/test",
express.static(join(__dirname, "../../frontend/dist"))
express.static(join(__dirname, "../frontend/dist"))
);
// register app

View File

@@ -10,10 +10,10 @@ import (
"github.com/gin-gonic/gin"
socketio "github.com/googollee/go-socket.io"
"github.com/joho/godotenv"
router "lst.net/internal/route_handler"
)
func main() {
err := godotenv.Load("../.env")
if err != nil {
fmt.Println("Warning: .env file not found")
@@ -29,10 +29,11 @@ func main() {
if os.Getenv("NODE_ENV") != "production" {
basePath = "/lst/api/controller"
}
r := router.Setup(basePath) // returns *gin.Engine
server := socketio.NewServer(nil)
r := Setup(basePath, server) // returns *gin.Engine
server.OnConnect("/", func(s socketio.Conn) error {
fmt.Println("✅ Client connected:", s.ID())
return nil

View File

@@ -1,20 +1,15 @@
package router
package main
import (
"fmt"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
socketio "github.com/googollee/go-socket.io"
)
type UpdatePayload struct {
Action string `json:"action"`
Target string `json:"target"`
}
func Setup(basePath string) *gin.Engine {
func Setup(basePath string, server *socketio.Server) *gin.Engine {
r := gin.Default()
@@ -44,12 +39,18 @@ func Setup(basePath string) *gin.Engine {
return
}
steps := []string{"🚀 Starting...", "📂 Copying...", "🔧 Migrating...", "✅ Done!"}
for _, step := range steps {
fmt.Fprintf(c.Writer, "event: log\ndata: %s\n\n", step)
flusher.Flush() // 🔑 actually push chunk
time.Sleep(1 * time.Second) // simulate work
// Start the update and get the channel
updates := UpdateApp(server)
for msg := range updates {
//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) })

305
controller/update.go Normal file
View 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
}

View File

@@ -11,6 +11,7 @@ import (
"os"
"strings"
"github.com/gin-gonic/gin"
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) {
switch strings.ToLower(payload.Action) {
case "copy":
server.BroadcastToRoom("/", "update", "updateLogs",
fmt.Sprintf("🚀 Copying latest build to %v", payload.Target))
copyLatestBuild(server, payload.Target)
case "update":
@@ -59,6 +58,7 @@ func updateServer(server *socketio.Server, target string) {
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.")
go UpdateApp(server)
return
}
@@ -80,16 +80,16 @@ func copyLatestBuild(server *socketio.Server, target string) {
func triggerRemoteUpdate(server *socketio.Server, remoteURL string, payload UpdatePayload) {
basePath := "/api/controller"
if os.Getenv("NODE_ENV") != "production" {
basePath = "/lst/api/controller"
}
// basePath := "/api/controller"
// if os.Getenv("NODE_ENV") != "production" {
// basePath = "/lst/api/controller"
// }
// send POST request with JSON, expect SSE / streaming text back
body, _ := json.Marshal(payload)
url := fmt.Sprintf("https://%v.alpla.net%v/update", remoteURL, basePath)
//url := fmt.Sprintf("http://localhost:8080%v/update", basePath)
//url := fmt.Sprintf("https://%v.alpla.net%v/update", remoteURL, basePath)
url := fmt.Sprintf("http://%v:8080/api/controller/update", remoteURL)
fmt.Println(url)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
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)
}
}

View File

@@ -11,21 +11,21 @@ param (
)
# Example string to run with the parameters in it.
# .C:\scripts\services.ps1 -serviceName "LST_ctl" -option "install" -appPath "E:\LST" -description "Logistics Support Tool controller" -command "E:\LST\controller\lst_ctl.exe"
# .\scripts\services.ps1 -serviceName "LST_ctl" -option "install" -appPath "E:\LST" -description "Logistics Support Tool controller" -command "E:\LST\controller\lst_ctl.exe"
# .\scripts\services.ps1 -serviceName "LST_app" -option "install" -appPath "E:\LST" -description "Logistics Support Tool" -command "run start"
$nssmPath = $AppPath + "\nssm.exe"
$npmPath = "C:\Program Files\nodejs\npm.cmd" # Path to npm.cmd
if($remote -eq "true"){
if ($remote -eq "true") {
# Convert the plain-text password to a SecureString
$securePass = ConvertTo-SecureString $admpass -AsPlainText -Force
$credentials = New-Object System.Management.Automation.PSCredential($username, $securePass)
# if(-not $username -or -not $admpass){
# Write-host "Missing adm account info please try again."
# exit 1
# }
# if(-not $username -or -not $admpass){
# Write-host "Missing adm account info please try again."
# exit 1
# }
$plantFunness = {
param ($service, $processType, $location)
@@ -37,22 +37,22 @@ if($remote -eq "true"){
exit 1
}
if(-not $service -or -not $processType){
if (-not $service -or -not $processType) {
Write-host "The service name or option is missing please enter one of them and try again."
exit 1
}
if ($processType -eq "start"){
if ($processType -eq "start") {
write-host "Starting $($service)."
Start-Service $service
}
if ($processType -eq "stop"){
if ($processType -eq "stop") {
write-host "Stoping $($service)."
Stop-Service $service
}
if ($processType -eq "restart"){
if ($processType -eq "restart") {
write-host "Stoping $($service) to be restarted"
Stop-Service $service
Start-Sleep 3 # so we give it enough time to fully stop
@@ -60,8 +60,8 @@ if($remote -eq "true"){
Start-Service $service
}
if ($processType -eq "prodStop"){
if(-not $location){
if ($processType -eq "prodStop") {
if (-not $location) {
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
@@ -72,8 +72,8 @@ if($remote -eq "true"){
}
if ($processType -eq "prodStart"){
if(-not $location){
if ($processType -eq "prodStart") {
if (-not $location) {
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
@@ -87,29 +87,30 @@ if($remote -eq "true"){
}
Invoke-Command -ComputerName $server -ScriptBlock $plantFunness -ArgumentList $serviceName, $option, $appPath -Credential $credentials
} else {
}
else {
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Host "Error: This script must be run as Administrator."
exit 1
}
if(-not $serviceName -or -not $option){
if (-not $serviceName -or -not $option) {
Write-host "The service name or option is missing please enter one of them and try again."
exit 1
}
if ($option -eq "start"){
if ($option -eq "start") {
write-host "Starting $($serviceName)."
Start-Service $serviceName
}
if ($option -eq "stop"){
if ($option -eq "stop") {
write-host "Stoping $($serviceName)."
Stop-Service $serviceName
}
if ($option -eq "restart"){
if ($option -eq "restart") {
write-host "Stoping $($serviceName) to be restarted"
Stop-Service $serviceName
Start-Sleep 3 # so we give it enough time to fully stop
@@ -117,8 +118,8 @@ if($remote -eq "true"){
Start-Service $serviceName
}
if ($option -eq "delete"){
if(-not $appPath){
if ($option -eq "delete") {
if (-not $appPath) {
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
@@ -128,8 +129,8 @@ if($remote -eq "true"){
}
if ($option -eq "prodStop"){
if(-not $appPath){
if ($option -eq "prodStop") {
if (-not $appPath) {
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
@@ -140,8 +141,8 @@ if($remote -eq "true"){
}
if ($option -eq "prodStart"){
if(-not $appPath){
if ($option -eq "prodStart") {
if (-not $appPath) {
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
@@ -152,8 +153,8 @@ if($remote -eq "true"){
}
if($option -eq "install"){
if(-not $appPath -or -not $description -or -not $command){
if ($option -eq "install") {
if (-not $appPath -or -not $description -or -not $command) {
Write-host "Please check all parameters are passed to install the app.."
exit 1
}
@@ -161,19 +162,20 @@ if($remote -eq "true"){
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if(-not $service){
if (-not $service) {
write-host $serviceName "is not installed we will install it now"
Write-Host "Installing $serviceName..."
if($command.Contains(".exe")){
if ($command.Contains(".exe")) {
& $nssmPath install $serviceName $command
$fullAppPath = "$appPath\app"
$fullAppPath = $appPath
& $nssmPath set $serviceName AppDirectory $fullAppPath
}else {
& $nssmPath install $serviceName $npmPath $command
& $nssmPath set $serviceName AppDirectory $appPath
}
else {
& $nssmPath install $serviceName $npmPath $command
& $nssmPath set $serviceName AppDirectory $appPath
}
@@ -184,20 +186,22 @@ if($remote -eq "true"){
# Set recovery options
sc.exe failure $serviceName reset= 0 actions= restart/5000/restart/5000/restart/5000
& $nssmPath start $serviceName
}else{
}
else {
write-host $serviceName "is already installed will push the updated info"
Write-Host "Updating $serviceName..."
& $nssmPath stop $serviceName
if($command.Contains(".exe")){
if ($command.Contains(".exe")) {
$fullAppPath = "$appPath\app"
& $nssmPath set $serviceName AppDirectory $fullAppPath
}else {
}
else {
& $nssmPath set $serviceName AppDirectory $appPath
}
& $nssmPath set $serviceName Description $description
# & $nssmPath set $serviceName DependOnService "IISADMIN MSSQLSERVER"
# & $nssmPath set $serviceName DependOnService "IISADMIN MSSQLSERVER"
# Set recovery options
sc.exe failure $serviceName reset= 0 actions= restart/5000/restart/5000/restart/5000
Start-Sleep 4

View File

@@ -16,7 +16,6 @@
},
"include": [
"app/src",
"scripts/**/*.ts",
"database/testFiles/test-tiPostOrders.ts",
"scripts/translateScript.js"
],
@@ -25,6 +24,7 @@
"frontend",
"dist",
"lstDocs",
"database/testFiles"
"database/testFiles",
"scripts"
]
}