Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
@@ -1,41 +1,82 @@
cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/caarlos0/go-version v0.2.2 h1:5r+nlrg4H2wOVwWjqRqRRIRbZ7ytRmjC9xoMIP0a5kQ=
github.com/caarlos0/go-version v0.2.2/go.mod h1:X+rI5VAtJDpcjCjeEIXpxGa5+rTcgur1FK66wS0/944=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/mango-cobra v1.3.0 h1:vQy5GvPg3ndOSpduxutqFoINhWk3vD5K2dXo5E8pqec=
github.com/muesli/mango-cobra v1.3.0/go.mod h1:Cj1ZrBu3806Qw7UjxnAUgE+7tllUBj1NCLQDwwGx19E=
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4=
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
mvdan.cc/editorconfig v0.3.0 h1:D1D2wLYEYGpawWT5SpM5pRivgEgXjtEXwC9MWhEY0gQ=
mvdan.cc/editorconfig v0.3.0/go.mod h1:NcJHuDtNOTEJ6251indKiWuzK6+VcrMuLzGMLKBFupQ=
1 change: 1 addition & 0 deletions v3/cmd/wails3/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func main() {

tool := app.NewSubCommand("tool", "Various tools")
tool.NewSubCommandFunction("checkport", "Checks if a port is open. Useful for testing if vite is running.", commands.ToolCheckPort)
tool.NewSubCommandFunction("waitport", "Waits for a port to open. Useful for gating dependent development tasks.", commands.ToolWaitPort)
tool.NewSubCommandFunction("watcher", "Watches files and runs a command when they change", commands.Watcher)
tool.NewSubCommandFunction("cp", "Copy files", commands.Cp)
tool.NewSubCommandFunction("buildinfo", "Show Build Info", commands.BuildInfo)
Expand Down
7 changes: 7 additions & 0 deletions v3/internal/commands/build_assets/Taskfile.tmpl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ tasks:
cmds:
- task: frontend:dev:{{.Opn}}.PACKAGE_MANAGER{{.Cls}}

dev:wait:
summary: Waits for the frontend development server
vars:
TIMEOUT: 60
cmds:
- wails3 tool waitport --timeout {{.Opn}}.TIMEOUT{{.Cls}}

frontend:dev:npm:
dir: frontend
cmds:
Expand Down
4 changes: 3 additions & 1 deletion v3/internal/commands/build_assets/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ dev_mode:
type: blocking
- cmd: wails3 task common:dev:frontend
type: background
- cmd: wails3 task common:dev:wait
type: once
- cmd: wails3 task run
type: primary

Expand All @@ -76,4 +78,4 @@ fileAssociations:

# Other data
other:
- name: My Other Data
- name: My Other Data
47 changes: 47 additions & 0 deletions v3/internal/commands/dev_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package commands

import (
"testing"

"github.com/atterpac/refresh/process"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestDevConfigGatesApplicationOnFrontendReadiness(t *testing.T) {
data, err := buildAssets.ReadFile("build_assets/config.yml")
require.NoError(t, err)

var config struct {
DevMode struct {
Executes []process.Execute `yaml:"executes"`
} `yaml:"dev_mode"`
}
require.NoError(t, yaml.Unmarshal(data, &config))

require.Len(t, config.DevMode.Executes, 4)
require.Equal(t, process.Background, config.DevMode.Executes[1].Type)
require.Equal(t, "wails3 task common:dev:wait", config.DevMode.Executes[2].Cmd)
require.Equal(t, process.Once, config.DevMode.Executes[2].Type)
require.Equal(t, process.Primary, config.DevMode.Executes[3].Type)
}

func TestFrontendDevServerTimeoutIsTaskLocal(t *testing.T) {
data, err := buildAssets.ReadFile("build_assets/Taskfile.tmpl.yml")
require.NoError(t, err)

var taskfile struct {
Tasks map[string]yaml.Node `yaml:"tasks"`
}
require.NoError(t, yaml.Unmarshal(data, &taskfile))

waitTaskNode, exists := taskfile.Tasks["dev:wait"]
require.True(t, exists)
var waitTask struct {
Vars map[string]int `yaml:"vars"`
Cmds []string `yaml:"cmds"`
}
require.NoError(t, waitTaskNode.Decode(&waitTask))
require.Equal(t, 60, waitTask.Vars["TIMEOUT"])
require.Equal(t, []string{"wails3 tool waitport --timeout {{.Opn}}.TIMEOUT{{.Cls}}"}, waitTask.Cmds)
}
63 changes: 63 additions & 0 deletions v3/internal/commands/tool_waitport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package commands

import (
"fmt"
"os"
"strconv"
"time"
)

const portWaitInterval = 100 * time.Millisecond

type ToolWaitPortOptions struct {
Host string `name:"h" description:"Host to check" default:"localhost"`
Port int `name:"p" description:"Port to check; defaults to WAILS_VITE_PORT when set"`
Timeout int `name:"timeout" description:"Maximum number of seconds to wait for the port to open" default:"60"`
}

func waitForPort(check func() bool, timeout time.Duration) bool {
if check() {
return true
}
if timeout <= 0 {
return false
}

ticker := time.NewTicker(portWaitInterval)
defer ticker.Stop()
timer := time.NewTimer(timeout)
defer timer.Stop()

for {
select {
case <-ticker.C:
if check() {
return true
}
case <-timer.C:
return false
}
}
}

func ToolWaitPort(options *ToolWaitPortOptions) error {
DisableFooter = true

if options.Port == 0 {
port := os.Getenv(wailsVitePort)
if port == "" {
return fmt.Errorf("please use the -p flag to specify a port or set %s", wailsVitePort)
}
var err error
options.Port, err = strconv.Atoi(port)
if err != nil {
return fmt.Errorf("invalid %s value %q: %w", wailsVitePort, port, err)
}
}

timeout := time.Duration(options.Timeout) * time.Second
if !waitForPort(func() bool { return isPortOpen(options.Host, options.Port) }, timeout) {
return fmt.Errorf("timed out after %s waiting for port %d to open on %s", timeout, options.Port, options.Host)
}
return nil
}
36 changes: 36 additions & 0 deletions v3/internal/commands/tool_waitport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package commands

import (
"testing"
"time"
)

func TestWaitForPortRetriesUntilReady(t *testing.T) {
attempts := 0
ready := waitForPort(func() bool {
attempts++
return attempts == 3
}, time.Second)

if !ready {
t.Fatal("waitForPort reported that the port was unavailable")
}
if attempts != 3 {
t.Fatalf("attempt count = %d, want 3", attempts)
}
}

func TestWaitForPortDoesNotRetryWithoutTimeout(t *testing.T) {
attempts := 0
ready := waitForPort(func() bool {
attempts++
return false
}, 0)

if ready {
t.Fatal("waitForPort reported that the port was available")
}
if attempts != 1 {
t.Fatalf("attempt count = %d, want 1", attempts)
}
}
14 changes: 7 additions & 7 deletions v3/internal/commands/watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@ package commands
import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEnsureIgnored(t *testing.T) {
t.Run("adds pattern when not present", func(t *testing.T) {
list := []string{".gitignore", ".DS_Store"}
ensureIgnored(&list, "*_test.go")
assert.Contains(t, list, "*_test.go")
assert.Len(t, list, 3)
require.Contains(t, list, "*_test.go")
require.Len(t, list, 3)
})

t.Run("does not duplicate pattern when already present", func(t *testing.T) {
list := []string{".gitignore", "*_test.go"}
ensureIgnored(&list, "*_test.go")
assert.Contains(t, list, "*_test.go")
assert.Len(t, list, 2)
require.Contains(t, list, "*_test.go")
require.Len(t, list, 2)
})

t.Run("adds to empty list", func(t *testing.T) {
var list []string
ensureIgnored(&list, "*_test.go")
assert.Contains(t, list, "*_test.go")
assert.Len(t, list, 1)
require.Contains(t, list, "*_test.go")
require.Len(t, list, 1)
})
}
19 changes: 6 additions & 13 deletions v3/pkg/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,12 +571,6 @@ func (a *App) Run() error {
// Ensure application context is cancelled in case of failures.
defer a.cancel()

// Call post-create hooks
err := a.preRun()
if err != nil {
return err
}

a.impl = newPlatformApp(a)

// Ensure services are shut down in case of failures.
Expand All @@ -602,13 +596,12 @@ func (a *App) Run() error {
a.options.Services = services[:i+1]
}


// Start the MCP server when the application is built with -tags mcp.
// All configuration is read from environment variables (WAILS_MCP_HOST,
// WAILS_MCP_PORT, WAILS_MCP_TIMEOUT, WAILS_MCP_HIDE_CURSOR).
if err := startMCPServer(a); err != nil {
return fmt.Errorf("mcp: %w", err)
}
// Start the MCP server when the application is built with -tags mcp.
// All configuration is read from environment variables (WAILS_MCP_HOST,
// WAILS_MCP_PORT, WAILS_MCP_TIMEOUT, WAILS_MCP_HIDE_CURSOR).
if err := startMCPServer(a); err != nil {
return fmt.Errorf("mcp: %w", err)
}

go func() {
for {
Expand Down
47 changes: 5 additions & 42 deletions v3/pkg/application/application_dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,13 @@

package application

import (
"net/http"
"time"

"github.com/wailsapp/wails/v3/internal/assetserver"
)

var devMode = false

func (a *App) preRun() error {
// Check for frontend server url
frontendURL := assetserver.GetDevServerURL()
if frontendURL != "" {
devMode = true
// We want to check if the frontend server is running by trying to http get the url
// and if it is not, we wait 500ms and try again for a maximum of 10 times. If it is
// still not available, we return an error.
// This is to allow the frontend server to start up before the backend server.
client := http.Client{}
a.Logger.Info("Waiting for frontend dev server to start...", "url", frontendURL)
for i := 0; i < 10; i++ {
_, err := client.Get(frontendURL)
if err == nil {
a.Logger.Info("Connected to frontend dev server!")
return nil
}
// Wait 500ms
time.Sleep(500 * time.Millisecond)
if i%2 == 0 {
a.Logger.Info("Retrying...")
}
}
a.fatal("unable to connect to frontend server. Please check it is running - FRONTEND_DEVSERVER_URL='%s'", frontendURL)
}
return nil
}
import "github.com/wailsapp/wails/v3/internal/assetserver"

func (a *App) postQuit() {
if devMode {
a.Logger.Info("The application has terminated, but the watcher is still running.")
a.Logger.Info("To terminate the watcher, press CTRL+C")
if assetserver.GetDevServerURL() == "" {
return
}
}

func (a *App) enableDevTools() {

a.Logger.Info("The application has terminated, but the watcher is still running.")
a.Logger.Info("To terminate the watcher, press CTRL+C")
}
Loading
Loading