diff --git a/v2/cmd/wails/internal/commands/dev/dev.go b/v2/cmd/wails/internal/commands/dev/dev.go index 3201fdcb..5adf1d71 100644 --- a/v2/cmd/wails/internal/commands/dev/dev.go +++ b/v2/cmd/wails/internal/commands/dev/dev.go @@ -1,6 +1,7 @@ package dev import ( + "context" "fmt" "io" "os" @@ -10,14 +11,18 @@ import ( "syscall" "time" - "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/pkg/commands/build" + + "github.com/wailsapp/wails/v2/internal/process" + + "github.com/wzshiming/ctc" + + "github.com/wailsapp/wails/v2/internal/fs" "github.com/fsnotify/fsnotify" + "github.com/leaanthony/clir" - "github.com/wailsapp/wails/v2/internal/fs" - "github.com/wailsapp/wails/v2/internal/process" "github.com/wailsapp/wails/v2/pkg/clilogger" - "github.com/wailsapp/wails/v2/pkg/commands/build" ) // AddSubcommand adds the `dev` command for the Wails application @@ -44,212 +49,262 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error { app.PrintBanner() // TODO: Check you are in a project directory - - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - defer watcher.Close() - - var debugBinaryProcess *process.Process = nil - var buildFrontend bool = false var extensionsThatTriggerARebuild = strings.Split(extensions, ",") - // Setup signal handler - quitChannel := make(chan os.Signal, 1) - signal.Notify(quitChannel, os.Interrupt, os.Kill, syscall.SIGTERM) - - debounceQuit := make(chan bool, 1) - - // Do initial build - logger.Println("Building application for development...") - debugBinaryProcess, err = restartApp(logger, "dev", ldflags, compilerCommand, debugBinaryProcess) - if err != nil { - return err - } - go debounce(100*time.Millisecond, watcher.Events, debounceQuit, func(event fsnotify.Event) { - // logger.Println("event: %+v", event) - - // Check for new directories - if event.Op&fsnotify.Create == fsnotify.Create { - // If this is a folder, add it to our watch list - if fs.DirExists(event.Name) { - if !strings.Contains(event.Name, "node_modules") { - err := watcher.Add(event.Name) - if err != nil { - logger.Fatal("%s", err.Error()) - } - logger.Println("Watching directory: %s", event.Name) - } - } - return - } - - // Check for file writes - if event.Op&fsnotify.Write == fsnotify.Write { - - logger.Println("modified file: %s", event.Name) - var rebuild bool = false - - // Iterate all file patterns - for _, pattern := range extensionsThatTriggerARebuild { - rebuild = strings.HasSuffix(event.Name, pattern) - if err != nil { - logger.Fatal(err.Error()) - } - if rebuild { - // Only build frontend when the file isn't a Go file - buildFrontend = !strings.HasSuffix(event.Name, "go") - break - } - } - - if !rebuild { - logger.Println("Filename change: %s did not match extension list (%s)", event.Name, extensions) - return - } - - logger.Println("Partial build triggered: %s updated", event.Name) - - // Do a rebuild - - // Try and build the app - newBinaryProcess, err := restartApp(logger, "dev", ldflags, compilerCommand, debugBinaryProcess) - if err != nil { - fmt.Printf("Error during build: %s", err.Error()) - return - } - // If we have a new process, save it - if newBinaryProcess != nil { - debugBinaryProcess = newBinaryProcess - } - - } - }) - - // Get project dir - dir, err := os.Getwd() + reloader, err := NewReloader(logger, extensionsThatTriggerARebuild, ldflags, compilerCommand) if err != nil { return err } - // Get all subdirectories - dirs, err := fs.GetSubdirectories(dir) + // Start + err = reloader.Start() if err != nil { - return err + println("ERRRRRRRRRR: %+v", err) } - // Setup a watcher for non-node_modules directories - dirs.Each(func(dir string) { - if strings.Contains(dir, "node_modules") { - return - } - logger.Println("Watching directory: %s", dir) - err = watcher.Add(dir) - if err != nil { - logger.Fatal(err.Error()) - } - }) + logger.Println("\nDevelopment mode exited") - // Wait until we get a quit signal - quit := false - for quit == false { - select { - case <-quitChannel: - println("Caught quit") - // Notify debouncer to quit - debounceQuit <- true - quit = true - } - } - - // Kill the current program if running - if debugBinaryProcess != nil { - err := debugBinaryProcess.Kill() - if err != nil { - return err - } - } - - logger.Println("Development mode exited") - - return nil + return err }) return nil } -// Credit: https://drailing.net/2018/01/debounce-function-for-golang/ -func debounce(interval time.Duration, input chan fsnotify.Event, quitChannel chan bool, cb func(arg fsnotify.Event)) { - var item fsnotify.Event - timer := time.NewTimer(interval) -exit: +type Reloader struct { + + // Main context + ctx context.Context + + // Signal context + signalContext context.Context + + // notify + watcher *fsnotify.Watcher + + // Logger + logger *clilogger.CLILogger + + // Extensions to listen for + extensionsThatTriggerARebuild []string + + // The binary we are running + binary *process.Process + + // options + ldflags string + compiler string +} + +func NewReloader(logger *clilogger.CLILogger, extensionsThatTriggerARebuild []string, ldFlags string, compiler string) (*Reloader, error) { + var result Reloader + + // Create context + result.ctx = context.Background() + + // Signal context (we don't need cancel) + signalContext, _ := signal.NotifyContext(result.ctx, os.Interrupt, os.Kill, syscall.SIGTERM) + result.signalContext = signalContext + + // Create watcher + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + result.watcher = watcher + + // Logger + result.logger = logger + + // Extensions + result.extensionsThatTriggerARebuild = extensionsThatTriggerARebuild + + // Options + result.ldflags = ldFlags + result.compiler = compiler + + return &result, nil +} + +func (r *Reloader) Start() error { + + err := r.rebuildBinary() + if err != nil { + return err + } + + // Get project dir + dir, err := os.Getwd() + if err != nil { + return err + } + + // Get all subdirectories + dirs, err := fs.GetSubdirectories(dir) + if err != nil { + return err + } + + // Setup a watcher for non-node_modules directories + r.logger.Println("Watching (sub)directories: %s", dir) + + dirs.Each(func(dir string) { + if strings.Contains(dir, "node_modules") { + return + } + err = r.watcher.Add(dir) + if err != nil { + r.logger.Fatal(err.Error()) + } + }) + + // Main loop for { select { - case item = <-input: - timer.Reset(interval) - case <-timer.C: - if item.Name != "" { - cb(item) + case <-r.signalContext.Done(): + if r.binary != nil { + println("Binary is not nil - kill") + return r.binary.Kill() + } + return nil + case event := <-r.watcher.Events: + err := r.processWatcherEvent(event) + if err != nil { + println("error from processWatcherEvent. Calling cancel()") + println("Calling kill") + return r.binary.Kill() } - case <-quitChannel: - break exit } } } -func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, debugBinaryProcess *process.Process) (*process.Process, error) { +func (r *Reloader) processWatcherEvent(event fsnotify.Event) error { - appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand) - println() - if err != nil { - logger.Fatal(err.Error()) - return nil, errors.Wrap(err, "Build Failed:") + // Check for new directories + if event.Op&fsnotify.Create == fsnotify.Create { + // If this is a folder, add it to our watch list + if fs.DirExists(event.Name) { + if !strings.Contains(event.Name, "node_modules") { + err := r.watcher.Add(event.Name) + if err != nil { + return err + } + r.logger.Println("Watching directory: %s", event.Name) + } + } + return nil } - logger.Println("Build new binary: %s", appBinary) - // Kill existing binary if need be - if debugBinaryProcess != nil { - killError := debugBinaryProcess.Kill() + // Check for file writes + if event.Op&fsnotify.Write == fsnotify.Write { - if killError != nil { - logger.Fatal("Unable to kill debug binary (PID: %d)!", debugBinaryProcess.PID()) + var rebuild bool + + // Iterate all file patterns + for _, pattern := range r.extensionsThatTriggerARebuild { + if strings.HasSuffix(event.Name, pattern) { + rebuild = true + } } - debugBinaryProcess = nil + if !rebuild { + return nil + } + + r.logger.Println("\n%s[Build triggered] %s %s", ctc.ForegroundGreen|ctc.ForegroundBright, event.Name, ctc.Reset) + + return r.rebuildBinary() + } + return nil +} + +func (r *Reloader) rebuildBinary() error { + + // rebuild binary + binary, err := r.buildApp() + if err != nil { + return err } - // TODO: Generate `backend.js` + // Kill current binary if running + if r.binary != nil { + err = r.binary.Kill() + if err != nil { + return err + } + } - // Start up new binary - newProcess := process.NewProcess(logger, appBinary) + newProcess := process.NewProcess(r.ctx, r.logger, binary) err = newProcess.Start() if err != nil { - // Remove binary - deleteError := fs.DeleteFile(appBinary) - if deleteError != nil { - logger.Fatal("Unable to delete app binary: " + appBinary) - } - logger.Fatal("Unable to start application: %s", err.Error()) + return err } - return newProcess, nil + // Ensure process runs correctly + + return nil } -func buildApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string) (string, error) { +//func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, debugBinaryProcess *process.Process) (*process.Process, error) { +// +// appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand) +// println() +// if err != nil { +// logger.Fatal(err.Error()) +// return nil, errors.Wrap(err, "Build Failed:") +// } +// logger.Println("Build new binary: %s", appBinary) +// +// // Kill existing binary if need be +// if debugBinaryProcess != nil { +// killError := debugBinaryProcess.Kill() +// +// if killError != nil { +// logger.Fatal("Unable to kill debug binary (PID: %d)!", debugBinaryProcess.PID()) +// } +// +// debugBinaryProcess = nil +// } +// +// // TODO: Generate `backend.js` +// +// // Start up new binary +// newProcess := process.NewProcess(logger, appBinary) +// err = newProcess.Start() +// if err != nil { +// // Remove binary +// deleteError := fs.DeleteFile(appBinary) +// if deleteError != nil { +// logger.Fatal("Unable to delete app binary: " + appBinary) +// } +// logger.Fatal("Unable to start application: %s", err.Error()) +// } +// +// // Check if port is open +// timeout := time.Second +// conn, err := net.DialTimeout("tcp", net.JoinHostPort("host", port), timeout) +// if err != nil { +// return +// } +// newProcess.Running +// return newProcess, nil +//} + +// buildapp attempts to compile the application +// It returns the path to the new binary or an error +func (r *Reloader) buildApp() (string, error) { // Create random output file outputFile := fmt.Sprintf("debug-%d", time.Now().Unix()) // Create BuildOptions buildOptions := &build.Options{ - Logger: logger, - OutputType: outputType, + Logger: r.logger, + OutputType: "dev", Mode: build.Debug, Pack: false, Platform: runtime.GOOS, - LDFlags: ldflags, - Compiler: compilerCommand, + LDFlags: r.ldflags, + Compiler: r.compiler, OutputFile: outputFile, IgnoreFrontend: true, } diff --git a/v2/cmd/wails/main.go b/v2/cmd/wails/main.go index 01eee20b..0d157edd 100644 --- a/v2/cmd/wails/main.go +++ b/v2/cmd/wails/main.go @@ -1,8 +1,11 @@ package main import ( + "fmt" "os" + "github.com/wzshiming/ctc" + "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/update" "github.com/leaanthony/clir" @@ -19,12 +22,30 @@ func fatal(message string) { os.Exit(1) } +func col(colour ctc.Color, text string) string { + return fmt.Sprintf("%s%s%s", colour, text, ctc.Reset) +} + +func Yellow(str string) string { + return col(ctc.ForegroundBrightYellow, str) +} + +func Red(str string) string { + return col(ctc.ForegroundBrightRed, str) +} + +func banner(cli *clir.Cli) string { + return fmt.Sprintf("%s %s - Go/HTML Application Framework", Yellow("Wails"), Red(version)) +} + func main() { var err error app := clir.NewCli("Wails", "Go/HTML Application Framework", version) + app.SetBannerFunction(banner) + build.AddBuildSubcommand(app, os.Stdout) err = initialise.AddSubcommand(app, os.Stdout) if err != nil { diff --git a/v2/go.mod b/v2/go.mod index a89cbcbe..924321e7 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -20,6 +20,7 @@ require ( github.com/tdewolff/minify v2.3.6+incompatible github.com/tdewolff/parse v2.3.4+incompatible // indirect github.com/tdewolff/test v1.0.6 // indirect + github.com/wzshiming/ctc v1.2.3 // indirect github.com/xyproto/xpm v1.2.1 golang.org/x/net v0.0.0-20200822124328-c89045814202 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a diff --git a/v2/go.sum b/v2/go.sum index b8cc83b8..7657a28d 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -78,6 +78,10 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/wzshiming/ctc v1.2.3 h1:q+hW3IQNsjIlOFBTGZZZeIXTElFM4grF4spW/errh/c= +github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28= +github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae h1:tpXvBXC3hpQBDCc9OojJZCQMVRAbT3TTdUMP8WguXkY= +github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20= github.com/xyproto/xpm v1.2.1 h1:trdvGjjWBsOOKzBBUPT6JvaIQM3acJEEYfbxN7M96wg= github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -97,6 +101,7 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c= diff --git a/v2/internal/app/debug.go b/v2/internal/app/debug.go index 22bc7cdc..7b76b0ed 100644 --- a/v2/internal/app/debug.go +++ b/v2/internal/app/debug.go @@ -4,8 +4,9 @@ package app import ( "flag" - "github.com/wailsapp/wails/v2/pkg/logger" "strings" + + "github.com/wailsapp/wails/v2/pkg/logger" ) // Init initialises the application for a debug environment diff --git a/v2/internal/process/process.go b/v2/internal/process/process.go index 8c1a99bf..afe595cf 100644 --- a/v2/internal/process/process.go +++ b/v2/internal/process/process.go @@ -1,6 +1,7 @@ package process import ( + "context" "os" "os/exec" @@ -9,18 +10,16 @@ import ( // Process defines a process that can be executed type Process struct { - logger *clilogger.CLILogger - cmd *exec.Cmd - exitChannel chan bool - Running bool + logger *clilogger.CLILogger + cmd *exec.Cmd + running bool } // NewProcess creates a new process struct -func NewProcess(logger *clilogger.CLILogger, cmd string, args ...string) *Process { +func NewProcess(ctx context.Context, logger *clilogger.CLILogger, cmd string, args ...string) *Process { result := &Process{ - logger: logger, - cmd: exec.Command(cmd, args...), - exitChannel: make(chan bool, 1), + logger: logger, + cmd: exec.CommandContext(ctx, cmd, args...), } result.cmd.Stdout = os.Stdout result.cmd.Stderr = os.Stderr @@ -35,30 +34,30 @@ func (p *Process) Start() error { return err } - p.Running = true + p.running = true - go func(cmd *exec.Cmd, running *bool, logger *clilogger.CLILogger, exitChannel chan bool) { - logger.Println("Starting process (PID: %d)", cmd.Process.Pid) - cmd.Wait() - logger.Println("Exiting process (PID: %d)", cmd.Process.Pid) - *running = false - exitChannel <- true - }(p.cmd, &p.Running, p.logger, p.exitChannel) + go func(p *Process) { + p.logger.Println("Starting process (PID: %d)", p.cmd.Process.Pid) + err := p.cmd.Wait() + if err != nil { + p.running = false + p.logger.Println("Process failed to run: %s", err.Error()) + return + } + p.logger.Println("Exiting process (PID: %d)", p.cmd.Process.Pid) + }(p) return nil } // Kill the process func (p *Process) Kill() error { - if !p.Running { - return nil + if p.running { + println("Calling kill") + p.running = false + return p.cmd.Process.Kill() } - err := p.cmd.Process.Kill() - - // Wait for command to exit properly - <-p.exitChannel - - return err + return nil } // PID returns the process PID diff --git a/v2/internal/runtime/js/runtime/bridge.js b/v2/internal/runtime/js/runtime/bridge.js index 6efce19b..d09ac7b4 100644 --- a/v2/internal/runtime/js/runtime/bridge.js +++ b/v2/internal/runtime/js/runtime/bridge.js @@ -779,18 +779,18 @@ function add_css$1() { var style = element("style"); - style.id = "svelte-1oysp7o-style"; - style.textContent = ".menu.svelte-1oysp7o.svelte-1oysp7o{padding:3px;background-color:#0008;color:#EEF;border-radius:5px;margin-top:5px;position:absolute;backdrop-filter:blur(3px) saturate(160%) contrast(45%) brightness(140%);border:1px solid rgb(88,88,88);box-shadow:0 0 1px rgb(146,146,148) inset}.menuitem.svelte-1oysp7o.svelte-1oysp7o{display:flex;align-items:center;padding:1px 5px}.menuitem.svelte-1oysp7o.svelte-1oysp7o:hover{display:flex;align-items:center;background-color:rgb(57,131,223);padding:1px 5px;border-radius:5px}.menuitem.svelte-1oysp7o img.svelte-1oysp7o{padding-right:5px}"; + style.id = "svelte-1ucacnf-style"; + style.textContent = ".menu.svelte-1ucacnf.svelte-1ucacnf{padding:5px;background-color:#0008;color:#EEF;border-radius:5px;margin-top:5px;position:absolute;backdrop-filter:blur(3px) saturate(160%) contrast(45%) brightness(140%);border:1px solid rgb(88,88,88);box-shadow:0 0 1px rgb(146,146,148) inset}.menuitem.svelte-1ucacnf.svelte-1ucacnf{display:flex;align-items:center;padding:1px 5px}.menuitem.svelte-1ucacnf.svelte-1ucacnf:hover{display:flex;align-items:center;background-color:rgb(57,131,223);padding:1px 5px;border-radius:5px}.menuitem.svelte-1ucacnf img.svelte-1ucacnf{padding-right:5px}.separator.svelte-1ucacnf.svelte-1ucacnf{padding-top:5px;width:100%;padding-bottom:5px}.separator.svelte-1ucacnf.svelte-1ucacnf:hover{background-color:#0000}"; append(document.head, style); } function get_each_context(ctx, list, i) { const child_ctx = ctx.slice(); - child_ctx[2] = list[i]; + child_ctx[3] = list[i]; return child_ctx; } - // (8:0) {#if !hidden} + // (14:0) {#if visible} function create_if_block$1(ctx) { let div; let if_block = /*menu*/ ctx[0].Menu && create_if_block_1(ctx); @@ -799,7 +799,7 @@ c() { div = element("div"); if (if_block) if_block.c(); - attr(div, "class", "menu svelte-1oysp7o"); + attr(div, "class", "menu svelte-1ucacnf"); }, m(target, anchor) { insert(target, div, anchor); @@ -826,7 +826,7 @@ }; } - // (10:4) {#if menu.Menu } + // (16:4) {#if menu.Menu } function create_if_block_1(ctx) { let each_1_anchor; let each_value = /*menu*/ ctx[0].Menu.Items; @@ -852,7 +852,7 @@ insert(target, each_1_anchor, anchor); }, p(ctx, dirty) { - if (dirty & /*menu*/ 1) { + if (dirty & /*click, menu*/ 1) { each_value = /*menu*/ ctx[0].Menu.Items; let i; @@ -882,44 +882,41 @@ }; } - // (13:12) {#if menuItem.Image } - function create_if_block_2(ctx) { + // (25:52) + function create_if_block_4(ctx) { let div; - let img; - let img_src_value; return { c() { div = element("div"); - img = element("img"); - attr(img, "alt", ""); - if (img.src !== (img_src_value = "data:image/png;base64," + /*menuItem*/ ctx[2].Image)) attr(img, "src", img_src_value); - attr(img, "class", "svelte-1oysp7o"); + div.innerHTML = `