optimize the reloadering, fix a bunch of issues with killing the processes

This commit is contained in:
maddalax 2024-09-26 11:40:31 -05:00
parent 8efeaf1c4c
commit 22a9ef8659
19 changed files with 230 additions and 101 deletions

View file

@ -1,4 +1,4 @@
package main
package internal
import (
"sync"

View file

@ -1,4 +1,4 @@
package main
package internal
import (
"log/slog"
@ -6,7 +6,7 @@ import (
"strings"
)
func getLogLevel() slog.Level {
func GetLogLevel() slog.Level {
// Get the log level from the environment variable
logLevel := os.Getenv("LOG_LEVEL")
switch strings.ToUpper(logLevel) {

View file

@ -4,6 +4,7 @@ import (
"bufio"
"flag"
"fmt"
"github.com/maddalax/htmgo/cli/htmgo/internal"
"github.com/maddalax/htmgo/cli/htmgo/tasks/astgen"
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
"github.com/maddalax/htmgo/cli/htmgo/tasks/css"
@ -48,7 +49,7 @@ func main() {
return
}
slog.SetLogLoggerLevel(getLogLevel())
slog.SetLogLoggerLevel(internal.GetLogLevel())
taskName := os.Args[1]
@ -63,7 +64,7 @@ func main() {
copyassets.CopyAssets()
fmt.Printf("Generating CSS...this may take a few seconds.\n")
fmt.Printf("Generating CSS...\n")
css.GenerateCss(process.ExitOnError)
wg := sync.WaitGroup{}
@ -82,11 +83,8 @@ func main() {
wg.Wait()
go func() {
css.GenerateCssWatch(process.ExitOnError)
}()
fmt.Printf("Starting server...\n")
process.KillAll()
go func() {
_ = run.Server()
}()

View file

@ -23,7 +23,8 @@ func RegisterSignals() chan bool {
fmt.Println("Received signal:", sig)
// Perform cleanup
fmt.Println("Cleaning up...")
process.KillAll()
process.OnShutdown()
// Signal that cleanup is done
done <- true
}()

View file

@ -62,8 +62,12 @@ func WriteFile(path string, cb func(content *ast.File) string) {
}
}
// Define the file path where you want to save the buffer
process.Run("git add "+path, process.Silent)
cmd := "git add " + path
process.Run(process.NewRawCommand(
cmd,
cmd,
process.Silent,
))
// Save the buffer to a file
err = os.WriteFile(path, bytes, 0644)

View file

@ -101,5 +101,6 @@ func CopyAssets() {
log.Fatalf("Error: %v", err)
}
process.Run(fmt.Sprintf("cd %s && git add .", destDirCss), process.Silent)
cmd := fmt.Sprintf("cd %s && git add .", destDirCss)
process.Run(process.NewRawCommand(cmd, cmd, process.Silent))
}

View file

@ -42,19 +42,8 @@ func GenerateCss(flags ...process.RunFlag) error {
return nil
}
exec := GetTailwindExecutableName()
return process.RunMany([]string{
fmt.Sprintf("%s -i ./assets/css/input.css -o ./assets/dist/main.css -c ./tailwind.config.js", exec),
}, append(flags, process.Silent)...)
}
func GenerateCssWatch(flags ...process.RunFlag) error {
if !Setup() {
return nil
}
exec := GetTailwindExecutableName()
return process.RunMany([]string{
fmt.Sprintf("%s -i ./assets/css/input.css -o ./assets/dist/main.css -c ./tailwind.config.js --watch=always", exec),
}, append(flags, process.KillOnlyOnExit, process.Silent)...)
cmd := fmt.Sprintf("%s -i ./assets/css/input.css -o ./assets/dist/main.css -c ./tailwind.config.js", exec)
return process.Run(process.NewRawCommand("tailwind", cmd, append(flags, process.Silent)...))
}
func downloadTailwindCli() {
@ -90,7 +79,9 @@ func downloadTailwindCli() {
}
fileName := fmt.Sprintf(`tailwindcss-%s`, distro)
url := fmt.Sprintf(`https://github.com/tailwindlabs/tailwindcss/releases/latest/download/%s`, fileName)
process.Run(fmt.Sprintf(`curl -LO %s`, url), process.ExitOnError)
cmd := fmt.Sprintf(`curl -LO %s`, url)
process.Run(process.NewRawCommand("tailwind-cli-download", cmd, process.ExitOnError))
outputFileName := GetTailwindExecutableName()
newPath := filepath.Join(process.GetWorkingDir(), outputFileName)
@ -100,7 +91,9 @@ func downloadTailwindCli() {
newPath)
if os != "windows" {
err = process.Run(fmt.Sprintf(`chmod +x %s`, newPath), process.ExitOnError)
err = process.Run(process.NewRawCommand("chmod-tailwind-cli",
fmt.Sprintf(`chmod +x %s`, newPath),
process.ExitOnError))
}
if err != nil {

View file

@ -43,7 +43,7 @@ func DownloadTemplate(outPath string) {
fmt.Printf("Downloading template %s\n to %s", templateName, tempOut)
err := process.Run("git clone https://github.com/maddalax/htmgo --depth=1 "+tempOut, process.ExitOnError)
err := process.Run(process.NewRawCommand("clone-template", "git clone https://github.com/maddalax/htmgo --depth=1 "+tempOut, process.ExitOnError))
if err != nil {
log.Fatalf("Error cloning the template, error: %s\n", err.Error())
@ -75,7 +75,7 @@ func DownloadTemplate(outPath string) {
}
for _, command := range commands {
process.Run(strings.Join(command, " "), process.ExitOnError)
process.Run(process.NewRawCommand("", strings.Join(command, " "), process.ExitOnError))
}
_ = util.ReplaceTextInFile(filepath.Join(newDir, "go.mod"),

View file

@ -4,16 +4,23 @@ package process
import (
"errors"
"log/slog"
"os"
"os/exec"
"syscall"
"time"
)
func KillProcess(process *os.Process) error {
if process == nil {
func KillProcess(process CmdWithFlags) error {
if process.Cmd == nil || process.Cmd.Process == nil {
return nil
}
return syscall.Kill(-process.Pid, syscall.SIGKILL)
slog.Debug("killing process",
slog.String("name", process.Name),
slog.Int("pid", process.Cmd.Process.Pid))
_ = syscall.Kill(-process.Cmd.Process.Pid, syscall.SIGKILL)
time.Sleep(time.Millisecond * 50)
return nil
}
func PrepareCommand(command *exec.Cmd) {

View file

@ -2,18 +2,17 @@ package process
import (
"fmt"
"os"
"os/exec"
"strconv"
"time"
)
import "golang.org/x/sys/windows"
func KillProcess(process *os.Process) error {
if process == nil {
func KillProcess(process CmdWithFlags) error {
if process.Cmd == nil || process.Cmd.Process == nil {
return nil
}
Run(fmt.Sprintf("taskkill /F /T /PID %s", strconv.Itoa(process.Pid)))
Run(NewRawCommand("killprocess", fmt.Sprintf("taskkill /F /T /PID %s", strconv.Itoa(process.Cmd.Process.Pid))))
time.Sleep(time.Millisecond * 50)
return nil
}

View file

@ -2,28 +2,49 @@ package process
import (
"fmt"
"github.com/maddalax/htmgo/cli/htmgo/internal"
"log/slog"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"sync"
"time"
)
type CmdWithFlags struct {
flags []RunFlag
cmd *exec.Cmd
Flags []RunFlag
Name string
Cmd *exec.Cmd
}
type RawCommand struct {
Name string
Args string
Flags []RunFlag
}
func NewRawCommand(name string, args string, flags ...RunFlag) RawCommand {
if name == "" {
name = args
}
c := RawCommand{Name: name, Args: args, Flags: flags}
if c.Flags == nil {
c.Flags = make([]RunFlag, 0)
}
return c
}
var workingDir string
var commands = make([]CmdWithFlags, 0)
var commands = make(map[string]CmdWithFlags)
func AppendRunning(cmd *exec.Cmd, flags ...RunFlag) {
func AppendRunning(cmd *exec.Cmd, raw RawCommand) {
slog.Debug("running", slog.String("command", strings.Join(cmd.Args, " ")),
slog.String("dir", cmd.Dir),
slog.String("cwd", GetWorkingDir()))
commands = append(commands, CmdWithFlags{flags: flags, cmd: cmd})
commands[raw.Name] = CmdWithFlags{Flags: raw.Flags, Name: raw.Name, Cmd: cmd}
}
func GetWorkingDir() string {
@ -51,19 +72,66 @@ func shouldSkipKilling(flags []RunFlag, skipFlag []RunFlag) bool {
return false
}
func StartLogger() {
if internal.GetLogLevel() != slog.LevelDebug {
return
}
go func() {
for {
time.Sleep(time.Second * 5)
items := make([]map[string]string, 0)
for _, cmd := range commands {
data := make(map[string]string)
data["command"] = fmt.Sprintf("%s %s", cmd.Cmd.Path, strings.Join(cmd.Cmd.Args, " "))
if cmd.Cmd.Process != nil {
data["pid"] = fmt.Sprintf("%d", cmd.Cmd.Process.Pid)
}
items = append(items, data)
}
fmt.Printf("Running processes:\n")
for i, item := range items {
fmt.Printf("%d: %+v\n", i, item)
}
fmt.Printf("\n")
}
}()
}
func GetProcessByName(name string) *CmdWithFlags {
for _, cmd := range commands {
if cmd.Name == name {
return &cmd
}
}
return nil
}
func OnShutdown() {
// request for shutdown
for _, cmd := range commands {
if cmd.Cmd != nil && cmd.Cmd.Process != nil {
cmd.Cmd.Process.Signal(os.Interrupt)
}
}
// give it a second
time.Sleep(time.Second * 2)
// force kill
KillAll()
}
func KillAll(skipFlag ...RunFlag) {
tries := 0
updatedCommands := make([]CmdWithFlags, 0)
updatedCommands := make(map[string]CmdWithFlags)
for {
tries++
allFinished := true
for _, cmd := range commands {
if cmd.cmd.Process == nil {
if cmd.Cmd.Process == nil {
allFinished = false
if tries > 50 {
args := strings.Join(cmd.cmd.Args, " ")
args := strings.Join(cmd.Cmd.Args, " ")
slog.Debug("process is not running after 50 tries, breaking.", slog.String("command", args))
allFinished = true
break
@ -72,7 +140,7 @@ func KillAll(skipFlag ...RunFlag) {
continue
}
} else {
updatedCommands = append(updatedCommands, cmd)
updatedCommands[cmd.Name] = cmd
}
}
if allFinished {
@ -80,18 +148,18 @@ func KillAll(skipFlag ...RunFlag) {
}
}
commands = make([]CmdWithFlags, 0)
commands = make(map[string]CmdWithFlags)
for _, command := range updatedCommands {
if command.cmd != nil && command.cmd.Process != nil {
commands = append(commands, command)
if command.Cmd != nil && command.Cmd.Process != nil {
commands[command.Name] = command
}
}
for _, command := range commands {
if shouldSkipKilling(command.flags, skipFlag) {
if shouldSkipKilling(command.Flags, skipFlag) {
continue
}
err := KillProcess(command.cmd.Process)
err := KillProcess(command)
if err != nil {
continue
}
@ -100,15 +168,15 @@ func KillAll(skipFlag ...RunFlag) {
for {
finished := true
for _, c := range commands {
if c.cmd.Process == nil {
if c.Cmd.Process == nil {
continue
}
if shouldSkipKilling(c.flags, skipFlag) {
if shouldSkipKilling(c.Flags, skipFlag) {
continue
}
exists := PidExists(int32(c.cmd.Process.Pid))
exists := PidExists(int32(c.Cmd.Process.Pid))
if exists {
KillProcess(c.cmd.Process)
KillProcess(c)
finished = false
}
}
@ -121,12 +189,13 @@ func KillAll(skipFlag ...RunFlag) {
}
}
commands = make([]CmdWithFlags, 0)
commands = make(map[string]CmdWithFlags)
slog.Debug("all processes killed\n")
}
func RunOrExit(command string) {
_ = Run(command, ExitOnError)
func RunOrExit(command RawCommand) {
command.Flags = append(command.Flags, ExitOnError)
_ = Run(command)
}
type RunFlag int
@ -137,11 +206,11 @@ const (
KillOnlyOnExit
)
func RunMany(commands []string, flags ...RunFlag) error {
func RunMany(commands ...RawCommand) error {
for _, command := range commands {
err := Run(command, flags...)
err := Run(command)
if err != nil {
if slices.Contains(flags, ExitOnError) {
if slices.Contains(command.Flags, ExitOnError) {
os.Exit(1)
}
return err
@ -150,19 +219,35 @@ func RunMany(commands []string, flags ...RunFlag) error {
return nil
}
func Run(command string, flags ...RunFlag) error {
parts := strings.Fields(command)
var mutex = &sync.Mutex{}
func Run(command RawCommand) error {
mutex.Lock()
parts := strings.Fields(command.Args)
args := make([]string, 0)
if len(parts) > 1 {
args = parts[1:]
}
cmd := exec.Command(parts[0], args...)
path := parts[0]
existing := GetProcessByName(command.Name)
if existing != nil {
slog.Debug("process already running, killing it", slog.String("command", command.Name))
KillProcess(*existing)
time.Sleep(time.Millisecond * 50)
} else {
slog.Debug("no existing process found for %s, safe to run...", slog.String("command", command.Name))
}
cmd := exec.Command(path, args...)
PrepareCommand(cmd)
if slices.Contains(flags, Silent) {
if slices.Contains(command.Flags, Silent) {
cmd.Stdout = nil
cmd.Stderr = nil
} else {
@ -174,16 +259,21 @@ func Run(command string, flags ...RunFlag) error {
cmd.Dir = workingDir
}
AppendRunning(cmd, flags...)
AppendRunning(cmd, command)
mutex.Unlock()
err := cmd.Run()
slog.Debug("command finished",
slog.String("command", command),
slog.String("command", command.Name),
slog.String("args", command.Args),
slog.String("dir", cmd.Dir),
slog.String("cwd", GetWorkingDir()),
slog.String("error", fmt.Sprintf("%v", err)))
delete(commands, command.Name)
if err == nil {
return nil
}
@ -192,10 +282,10 @@ func Run(command string, flags ...RunFlag) error {
return nil
}
if slices.Contains(flags, ExitOnError) {
if slices.Contains(command.Flags, ExitOnError) {
slog.Error("Error running command: ",
slog.String("error", err.Error()),
slog.String("command", command))
slog.String("command", command.Name))
os.Exit(1)
}

View file

@ -4,7 +4,7 @@ import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/maddalax/htmgo/cli/htmgo/tasks/astgen"
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
"github.com/maddalax/htmgo/cli/htmgo/tasks/css"
"github.com/maddalax/htmgo/cli/htmgo/tasks/run"
"github.com/maddalax/htmgo/cli/htmgo/tasks/util"
"log/slog"
@ -60,9 +60,10 @@ type Tasks struct {
AstGen bool
Run bool
Ent bool
Css bool
}
func OnFileChange(events []*fsnotify.Event) {
func OnFileChange(version string, events []*fsnotify.Event) {
now := time.Now()
tasks := Tasks{}
@ -71,11 +72,13 @@ func OnFileChange(events []*fsnotify.Event) {
for _, event := range events {
c := NewChange(event)
if c.IsGenerated() {
if c.HasAnySuffix(".go~", ".css~") {
continue
}
slog.Debug("file changed", slog.String("file", c.Name()))
if c.IsGenerated() {
continue
}
if c.IsGo() && c.HasAnyPrefix("pages/", "partials/") {
tasks.AstGen = true
@ -84,6 +87,7 @@ func OnFileChange(events []*fsnotify.Event) {
if c.IsGo() {
tasks.Run = true
tasks.Css = true
hasTask = true
}
@ -92,18 +96,13 @@ func OnFileChange(events []*fsnotify.Event) {
hasTask = true
}
if c.HasAnySuffix("tailwind.config.js", ".css") {
tasks.Run = true
hasTask = true
}
if c.HasAnyPrefix("ent/schema") {
tasks.Ent = true
hasTask = true
}
if hasTask {
slog.Info("file changed", slog.String("file", c.Name()))
slog.Info("file changed", slog.String("version", version), slog.String("file", c.Name()))
}
}
@ -122,6 +121,15 @@ func OnFileChange(events []*fsnotify.Event) {
}()
}
if tasks.Css {
deps = append(deps, func() any {
return util.Trace("generate css", func() any {
css.GenerateCss()
return nil
})
})
}
if tasks.Ent {
deps = append(deps, func() any {
return util.Trace("generate ent", func() any {
@ -146,13 +154,6 @@ func OnFileChange(events []*fsnotify.Event) {
wg.Wait()
if tasks.Run {
util.Trace("kill all processes", func() any {
process.KillAll(process.KillOnlyOnExit)
return nil
})
}
if tasks.Run {
go run.Server()
}

View file

@ -14,10 +14,10 @@ func Build() {
astgen.GenAst(process.ExitOnError)
css.GenerateCss(process.ExitOnError)
process.RunOrExit("mkdir -p ./dist")
process.RunOrExit(process.NewRawCommand("", "mkdir -p ./dist"))
if os.Getenv("SKIP_GO_BUILD") != "1" {
process.RunOrExit(fmt.Sprintf("go build -tags prod -o ./dist"))
process.RunOrExit(process.NewRawCommand("", fmt.Sprintf("go build -tags prod -o ./dist")))
}
fmt.Printf("Executable built at %s\n", process.GetPathRelativeToCwd("dist"))

View file

@ -7,15 +7,15 @@ import (
)
func EntNewSchema(name string) {
process.RunOrExit("GOWORK=off go run -mod=mod entgo.io/ent/cmd/ent new " + name)
process.RunOrExit(process.NewRawCommand("", "GOWORK=off go run -mod=mod entgo.io/ent/cmd/ent new "+name))
}
func EntGenerate() {
if dirutil.HasFileFromRoot("ent/schema") {
if runtime.GOOS == "windows" {
process.RunOrExit("go generate ./ent")
process.RunOrExit(process.NewRawCommand("ent-generate", "go generate ./ent"))
} else {
process.RunOrExit("bash -c GOWORK=off go generate ./ent")
process.RunOrExit(process.NewRawCommand("ent-generate", "bash -c GOWORK=off go generate ./ent"))
}
}
}

View file

@ -3,5 +3,5 @@ package run
import "github.com/maddalax/htmgo/cli/htmgo/tasks/process"
func Server(flags ...process.RunFlag) error {
return process.Run("go run .", flags...)
return process.Run(process.NewRawCommand("run-server", "go run .", flags...))
}

View file

@ -8,8 +8,8 @@ import (
)
func Setup() {
process.RunOrExit("go mod download")
process.RunOrExit("go mod tidy")
process.RunOrExit(process.NewRawCommand("", "go mod download"))
process.RunOrExit(process.NewRawCommand("", "go mod tidy"))
copyassets.CopyAssets()
astgen.GenAst(process.ExitOnError)

View file

@ -2,16 +2,20 @@ package main
import (
"github.com/fsnotify/fsnotify"
"github.com/google/uuid"
"github.com/maddalax/htmgo/cli/htmgo/internal"
"log"
"log/slog"
"os"
"path/filepath"
"time"
)
var ignoredDirs = []string{".git", ".idea", "node_modules", "vendor"}
func startWatcher(cb func(file []*fsnotify.Event)) {
func startWatcher(cb func(version string, file []*fsnotify.Event)) {
events := make([]*fsnotify.Event, 0)
debouncer := internal.NewDebouncer(500 * time.Millisecond)
defer func() {
if r := recover(); r != nil {
@ -34,8 +38,18 @@ func startWatcher(cb func(file []*fsnotify.Event)) {
}
if event.Has(fsnotify.Write) || event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) {
events = append(events, &event)
cb(events)
events = make([]*fsnotify.Event, 0)
debouncer.Do(func() {
seen := make(map[string]bool)
dedupe := make([]*fsnotify.Event, 0)
for _, e := range events {
if _, ok := seen[e.Name]; !ok {
seen[e.Name] = true
dedupe = append(dedupe, e)
}
}
cb(uuid.NewString()[0:6], dedupe)
events = make([]*fsnotify.Event, 0)
})
}
case err, ok := <-watcher.Errors:
if !ok {

View file

@ -1,9 +1,14 @@
package h
import (
"fmt"
"github.com/labstack/echo/v4"
"github.com/maddalax/htmgo/framework/hx"
"github.com/maddalax/htmgo/framework/service"
"log/slog"
"os/exec"
"runtime"
"time"
)
type RequestContext struct {
@ -86,6 +91,25 @@ func (a App) start() {
err := a.Echo.Start(port)
if err != nil {
// If we are in watch mode, just try to kill any processes holding that port
// and try again
if IsDevelopment() && IsWatchMode() {
slog.Info("Port already in use, trying to kill the process and start again")
if runtime.GOOS == "windows" {
cmd := exec.Command("cmd", "/C", fmt.Sprintf(`for /F "tokens=5" %%i in ('netstat -aon ^| findstr :%s') do taskkill /F /PID %%i`, port))
cmd.Run()
} else {
cmd := exec.Command("bash", "-c", fmt.Sprintf("kill -9 $(lsof -ti%s)", port))
cmd.Run()
}
time.Sleep(time.Millisecond * 50)
err = a.Echo.Start(port)
if err != nil {
panic(err)
}
} else {
panic(err)
}
panic(err)
}
}

View file

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;