forked from aws/copilot-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: run local --watch flag (aws#5413)
Implements the `--watch` flag for `run local` which watches your copilot workspace for file changes and restarts the docker containers when you make changes. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the Apache 2.0 License.
- Loading branch information
1 parent
e5aebd9
commit 90d3af3
Showing
11 changed files
with
774 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package filetest | ||
|
||
import "github.com/fsnotify/fsnotify" | ||
|
||
// Double is a test double for file.RecursiveWatcher | ||
type Double struct { | ||
EventsFn func() <-chan fsnotify.Event | ||
ErrorsFn func() <-chan error | ||
} | ||
|
||
// Add is a no-op for Double. | ||
func (d *Double) Add(string) error { | ||
return nil | ||
} | ||
|
||
// Close is a no-op for Double. | ||
func (d *Double) Close() error { | ||
return nil | ||
} | ||
|
||
// Events calls the stubbed function. | ||
func (d *Double) Events() <-chan fsnotify.Event { | ||
if d.EventsFn == nil { | ||
return nil | ||
} | ||
return d.EventsFn() | ||
} | ||
|
||
// Errors calls the stubbed function. | ||
func (d *Double) Errors() <-chan error { | ||
if d.ErrorsFn == nil { | ||
return nil | ||
} | ||
return d.ErrorsFn() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
//go:build !windows | ||
|
||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package file | ||
|
||
import "path/filepath" | ||
|
||
// IsHiddenFile returns true if the file is hidden on non-windows. The filename must be non-empty. | ||
func IsHiddenFile(filename string) (bool, error) { | ||
return filepath.Base(filename)[0] == '.', nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package file | ||
|
||
import ( | ||
"syscall" | ||
) | ||
|
||
// IsHiddenFile returns true if the file is hidden on windows. | ||
func IsHiddenFile(filename string) (bool, error) { | ||
pointer, err := syscall.UTF16PtrFromString(filename) | ||
if err != nil { | ||
return false, err | ||
} | ||
attributes, err := syscall.GetFileAttributes(pointer) | ||
if err != nil { | ||
return false, err | ||
} | ||
return attributes&syscall.FILE_ATTRIBUTE_HIDDEN != 0, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package file | ||
|
||
import ( | ||
"io/fs" | ||
"path/filepath" | ||
|
||
"github.com/fsnotify/fsnotify" | ||
) | ||
|
||
// RecursiveWatcher wraps an fsnotify Watcher to recursively watch all files in a directory. | ||
type RecursiveWatcher struct { | ||
fsnotifyWatcher *fsnotify.Watcher | ||
done chan struct{} | ||
closed bool | ||
events chan fsnotify.Event | ||
errors chan error | ||
} | ||
|
||
// NewRecursiveWatcher returns a RecursiveWatcher which notifies when changes are made to files inside a recursive directory tree. | ||
func NewRecursiveWatcher(buffer uint) (*RecursiveWatcher, error) { | ||
watcher, err := fsnotify.NewBufferedWatcher(buffer) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
rw := &RecursiveWatcher{ | ||
events: make(chan fsnotify.Event, buffer), | ||
errors: make(chan error), | ||
fsnotifyWatcher: watcher, | ||
done: make(chan struct{}), | ||
closed: false, | ||
} | ||
|
||
go rw.start() | ||
|
||
return rw, nil | ||
} | ||
|
||
// Add recursively adds a directory tree to the list of watched files. | ||
func (rw *RecursiveWatcher) Add(path string) error { | ||
if rw.closed { | ||
return fsnotify.ErrClosed | ||
} | ||
return filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error { | ||
if err != nil { | ||
// swallow error from WalkDir, don't attempt to add to watcher. | ||
return nil | ||
} | ||
if d.IsDir() { | ||
return rw.fsnotifyWatcher.Add(p) | ||
} | ||
return nil | ||
}) | ||
} | ||
|
||
// Events returns the events channel. | ||
func (rw *RecursiveWatcher) Events() <-chan fsnotify.Event { | ||
return rw.events | ||
} | ||
|
||
// Errors returns the errors channel. | ||
func (rw *RecursiveWatcher) Errors() <-chan error { | ||
return rw.errors | ||
} | ||
|
||
// Close closes the RecursiveWatcher. | ||
func (rw *RecursiveWatcher) Close() error { | ||
if rw.closed { | ||
return nil | ||
} | ||
rw.closed = true | ||
close(rw.done) | ||
return rw.fsnotifyWatcher.Close() | ||
} | ||
|
||
func (rw *RecursiveWatcher) start() { | ||
for { | ||
select { | ||
case <-rw.done: | ||
close(rw.events) | ||
close(rw.errors) | ||
return | ||
case event := <-rw.fsnotifyWatcher.Events: | ||
// handle recursive watch | ||
switch event.Op { | ||
case fsnotify.Create: | ||
if err := rw.Add(event.Name); err != nil { | ||
rw.errors <- err | ||
} | ||
} | ||
|
||
rw.events <- event | ||
case err := <-rw.fsnotifyWatcher.Errors: | ||
rw.errors <- err | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
//go:build integration || localintegration | ||
|
||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package file_test | ||
|
||
import ( | ||
"fmt" | ||
"io/fs" | ||
"os" | ||
"testing" | ||
"time" | ||
|
||
"github.com/aws/copilot-cli/internal/pkg/cli/file" | ||
"github.com/fsnotify/fsnotify" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestRecursiveWatcher(t *testing.T) { | ||
var ( | ||
watcher *file.RecursiveWatcher | ||
tmp string | ||
eventsExpected []fsnotify.Event | ||
eventsActual []fsnotify.Event | ||
) | ||
|
||
tmp = os.TempDir() | ||
eventsActual = make([]fsnotify.Event, 0) | ||
eventsExpected = []fsnotify.Event{ | ||
{ | ||
Name: fmt.Sprintf("%s/watch/subdir/testfile", tmp), | ||
Op: fsnotify.Create, | ||
}, | ||
{ | ||
Name: fmt.Sprintf("%s/watch/subdir/testfile", tmp), | ||
Op: fsnotify.Chmod, | ||
}, | ||
{ | ||
Name: fmt.Sprintf("%s/watch/subdir/testfile", tmp), | ||
Op: fsnotify.Write, | ||
}, | ||
{ | ||
Name: fmt.Sprintf("%s/watch/subdir/testfile", tmp), | ||
Op: fsnotify.Write, | ||
}, | ||
{ | ||
Name: fmt.Sprintf("%s/watch/subdir", tmp), | ||
Op: fsnotify.Rename, | ||
}, | ||
{ | ||
Name: fmt.Sprintf("%s/watch/subdir2", tmp), | ||
Op: fsnotify.Create, | ||
}, | ||
{ | ||
Name: fmt.Sprintf("%s/watch/subdir", tmp), | ||
Op: fsnotify.Rename, | ||
}, | ||
{ | ||
Name: fmt.Sprintf("%s/watch/subdir2/testfile", tmp), | ||
Op: fsnotify.Rename, | ||
}, | ||
{ | ||
Name: fmt.Sprintf("%s/watch/subdir2/testfile2", tmp), | ||
Op: fsnotify.Create, | ||
}, | ||
{ | ||
Name: fmt.Sprintf("%s/watch/subdir2/testfile2", tmp), | ||
Op: fsnotify.Remove, | ||
}, | ||
} | ||
|
||
t.Run("Setup Watcher", func(t *testing.T) { | ||
err := os.MkdirAll(fmt.Sprintf("%s/watch/subdir", tmp), 0755) | ||
require.NoError(t, err) | ||
|
||
watcher, err = file.NewRecursiveWatcher(uint(len(eventsExpected))) | ||
require.NoError(t, err) | ||
}) | ||
|
||
t.Run("Watch", func(t *testing.T) { | ||
// SETUP | ||
err := watcher.Add(fmt.Sprintf("%s/watch", tmp)) | ||
require.NoError(t, err) | ||
|
||
eventsCh := watcher.Events() | ||
errorsCh := watcher.Errors() | ||
|
||
expectEvents := func(t *testing.T, n int) []fsnotify.Event { | ||
receivedEvents := []fsnotify.Event{} | ||
for i := 0; i < n; i++ { | ||
select { | ||
case e := <-eventsCh: | ||
receivedEvents = append(receivedEvents, e) | ||
case <-time.After(time.Second): | ||
} | ||
} | ||
return receivedEvents | ||
} | ||
|
||
// WATCH | ||
file, err := os.Create(fmt.Sprintf("%s/watch/subdir/testfile", tmp)) | ||
require.NoError(t, err) | ||
eventsActual = append(eventsActual, expectEvents(t, 1)...) | ||
|
||
err = os.Chmod(fmt.Sprintf("%s/watch/subdir/testfile", tmp), 0755) | ||
require.NoError(t, err) | ||
eventsActual = append(eventsActual, expectEvents(t, 1)...) | ||
|
||
err = os.WriteFile(fmt.Sprintf("%s/watch/subdir/testfile", tmp), []byte("write to file"), fs.ModeAppend) | ||
require.NoError(t, err) | ||
eventsActual = append(eventsActual, expectEvents(t, 2)...) | ||
|
||
err = file.Close() | ||
require.NoError(t, err) | ||
|
||
err = os.Rename(fmt.Sprintf("%s/watch/subdir", tmp), fmt.Sprintf("%s/watch/subdir2", tmp)) | ||
require.NoError(t, err) | ||
eventsActual = append(eventsActual, expectEvents(t, 3)...) | ||
|
||
err = os.Rename(fmt.Sprintf("%s/watch/subdir2/testfile", tmp), fmt.Sprintf("%s/watch/subdir2/testfile2", tmp)) | ||
require.NoError(t, err) | ||
eventsActual = append(eventsActual, expectEvents(t, 2)...) | ||
|
||
err = os.Remove(fmt.Sprintf("%s/watch/subdir2/testfile2", tmp)) | ||
require.NoError(t, err) | ||
eventsActual = append(eventsActual, expectEvents(t, 1)...) | ||
|
||
// CLOSE | ||
err = watcher.Close() | ||
require.NoError(t, err) | ||
require.Empty(t, errorsCh) | ||
|
||
require.Equal(t, eventsExpected, eventsActual) | ||
}) | ||
|
||
t.Run("Clean", func(t *testing.T) { | ||
err := os.RemoveAll(fmt.Sprintf("%s/watch", tmp)) | ||
require.NoError(t, err) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.