From 4e05fb091158e4cd9619c42bb922845bb0d48d86 Mon Sep 17 00:00:00 2001 From: connerdouglass Date: Tue, 25 Jun 2024 14:01:26 -0400 Subject: [PATCH] feat: rebuild plugin on file changes while running dev server --- cmd/cmd_run.go | 87 ++++++++++++++++++++++++++++++---- cmd/cmd_yml.go | 2 +- cmd/main.go | 10 ++-- go.mod | 8 +++- go.sum | 6 +++ internal/minecraft/versions.go | 3 +- internal/serve/serve.go | 46 ++++++++++++++---- 7 files changed, 137 insertions(+), 25 deletions(-) diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go index a940ebe..3e1a3ff 100644 --- a/cmd/cmd_run.go +++ b/cmd/cmd_run.go @@ -1,17 +1,22 @@ package main import ( + "errors" + "log" "os" + "path/filepath" "github.com/customrealms/cli/internal/build" "github.com/customrealms/cli/internal/project" "github.com/customrealms/cli/internal/serve" "github.com/customrealms/cli/internal/server" + "github.com/fsnotify/fsnotify" + "golang.org/x/sync/errgroup" ) type RunCmd struct { ProjectDir string `name:"project" short:"p" usage:"plugin project directory" optional:""` - McVersion string `name:"mc" short:"mc" usage:"Minecraft version number target" optional:""` + McVersion string `name:"mc" usage:"Minecraft version number target" optional:""` TemplateJarFile string `name:"jar" short:"t" usage:"template JAR file" optional:""` } @@ -70,11 +75,77 @@ func (c *RunCmd) Run() error { return err } - // Create the serve runner - serveAction := serve.ServeAction{ - MinecraftVersion: minecraftVersion, - PluginJarPath: outputFile, - ServerJarFetcher: serverJarFetcher, - } - return serveAction.Run(ctx) + eg, ctx := errgroup.WithContext(ctx) + + chanPluginUpdated := make(chan struct{}) + chanServerStopped := make(chan struct{}) + eg.Go(func() error { + // Create new watcher. + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + defer watcher.Close() + + // Start listening for events. + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Has(fsnotify.Write) { + // Rebuild the plugin JAR file + if err := buildAction.Run(ctx); err != nil { + log.Println("Error: ", err) + } else { + select { + case chanPluginUpdated <- struct{}{}: + case <-ctx.Done(): + return + } + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("error:", err) + case <-ctx.Done(): + return + case <-chanServerStopped: + return + } + } + }() + + // Add the project directory and src directory to the watcher + if err := errors.Join( + watcher.Add(c.ProjectDir), + watcher.Add(filepath.Join(c.ProjectDir, "src")), + ); err != nil { + return err + } + + // Block until the server is stopped + select { + case <-ctx.Done(): + return ctx.Err() + case <-chanServerStopped: + return nil + } + }) + eg.Go(func() error { + defer close(chanServerStopped) + + // Create the serve runner + serveAction := serve.ServeAction{ + MinecraftVersion: minecraftVersion, + PluginJarPath: outputFile, + ServerJarFetcher: serverJarFetcher, + } + return serveAction.Run(ctx, chanPluginUpdated) + }) + return eg.Wait() } diff --git a/cmd/cmd_yml.go b/cmd/cmd_yml.go index d6fb9f2..16bd95c 100644 --- a/cmd/cmd_yml.go +++ b/cmd/cmd_yml.go @@ -10,7 +10,7 @@ import ( type YmlCmd struct { ProjectDir string `name:"project" short:"p" usage:"plugin project directory" optional:""` - McVersion string `name:"mc" short:"mc" usage:"Minecraft version number target" optional:""` + McVersion string `name:"mc" usage:"Minecraft version number target" optional:""` } func (c *YmlCmd) Run() error { diff --git a/cmd/main.go b/cmd/main.go index e31ec14..4027766 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -12,11 +12,11 @@ import ( ) var cli struct { - VersionCmd VersionCmd `cmd:"" help:"Show the version of the CLI."` - InitCmd InitCmd `cmd:"" help:"Initialize a new plugin project."` - BuildCmd BuildCmd `cmd:"" help:"Build the plugin JAR file."` - RunCmd RunCmd `cmd:"" help:"Build and serve the plugin in a Minecraft server."` - YmlCmd YmlCmd `cmd:"" help:"Generate the plugin.yml file."` + VersionCmd VersionCmd `cmd:"" name:"version" help:"Show the version of the CLI."` + InitCmd InitCmd `cmd:"" name:"init" help:"Initialize a new plugin project."` + BuildCmd BuildCmd `cmd:"" name:"build" help:"Build the plugin JAR file."` + RunCmd RunCmd `cmd:"" name:"run" help:"Build and serve the plugin in a Minecraft server."` + YmlCmd YmlCmd `cmd:"" name:"yml" help:"Generate the plugin.yml file."` } func rootContext() (context.Context, context.CancelFunc) { diff --git a/go.mod b/go.mod index a2a9c5d..663c4bb 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,10 @@ module github.com/customrealms/cli go 1.22 -require github.com/alecthomas/kong v0.9.0 +require ( + github.com/alecthomas/kong v0.9.0 + github.com/fsnotify/fsnotify v1.7.0 + golang.org/x/sync v0.7.0 +) + +require golang.org/x/sys v0.4.0 // indirect diff --git a/go.sum b/go.sum index 524151d..1518502 100644 --- a/go.sum +++ b/go.sum @@ -4,5 +4,11 @@ github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/minecraft/versions.go b/internal/minecraft/versions.go index f102b4f..713adcb 100644 --- a/internal/minecraft/versions.go +++ b/internal/minecraft/versions.go @@ -24,7 +24,7 @@ type paperMcBuild struct { func LookupVersion(ctx context.Context, versionStr string) (Version, error) { // Lookup the version from PaperMC - builds, err := downloadJSON[paperMcBuilds](ctx, fmt.Sprintf("https://papermc.io/api/v2/projects/paper/versions/%s", versionStr)) + builds, err := downloadJSON[paperMcBuilds](ctx, fmt.Sprintf("https://papermc.io/api/v2/projects/paper/versions/%s/builds", versionStr)) if err != nil { return nil, fmt.Errorf("download builds list: %w", err) } @@ -48,6 +48,7 @@ func LookupVersion(ctx context.Context, versionStr string) (Version, error) { } func downloadJSON[T any](ctx context.Context, url string) (*T, error) { + fmt.Println(url) // Create the HTTP request req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 75f3673..f273898 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -10,6 +10,7 @@ import ( "github.com/customrealms/cli/internal/minecraft" "github.com/customrealms/cli/internal/server" + "golang.org/x/sync/errgroup" ) type ServeAction struct { @@ -61,7 +62,7 @@ func copyFile(from, to string) error { return nil } -func (a *ServeAction) Run(ctx context.Context) error { +func (a *ServeAction) Run(ctx context.Context, chanPluginUpdated <-chan struct{}) error { // Check if Java is installed on the machine if _, err := exec.LookPath("java"); err != nil { @@ -126,12 +127,39 @@ func (a *ServeAction) Run(ctx context.Context) error { fmt.Println("============================================================") fmt.Println() - // Run the server - cmd := exec.CommandContext(ctx, "java", "-jar", jarBase, "-nogui") - cmd.Dir = dir - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - + eg, ctx := errgroup.WithContext(ctx) + + chanServerStopped := make(chan struct{}) + eg.Go(func() error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-chanServerStopped: + return nil + case _, ok := <-chanPluginUpdated: + if !ok { + return nil + } + } + + // Copy the plugin file to the server + if err := copyFile(a.PluginJarPath, filepath.Join(pluginsDir, filepath.Base(a.PluginJarPath))); err != nil { + return err + } + fmt.Println("Plugin JAR updated. Run `/reload confirm` to reload the plugin.") + } + }) + eg.Go(func() error { + defer close(chanServerStopped) + + // Run the server + cmd := exec.CommandContext(ctx, "java", "-jar", jarBase, "-nogui") + cmd.Dir = dir + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + }) + return eg.Wait() }