feat: Add option to ignore dangerous root dir protections (#862)

This commit is contained in:
Andrew Borg
2026-01-14 00:32:28 -06:00
committed by GitHub
parent f4d163b3cf
commit e4c89c110d
3 changed files with 150 additions and 28 deletions
+2
View File
@@ -36,6 +36,8 @@ exclude_file = []
exclude_regex = ["_test\\.go"]
# Exclude unchanged files.
exclude_unchanged = true
# Ignore dangerous root directory that could cause excessive file watching
ignore_dangerous_root_dir = false
# Follow symlink for directories
follow_symlink = true
# This log file is placed in your tmp_dir.
+32 -28
View File
@@ -80,33 +80,34 @@ func (e entrypoint) args() []string {
}
type cfgBuild struct {
PreCmd []string `toml:"pre_cmd" usage:"Array of commands to run before each build"`
Cmd string `toml:"cmd" usage:"Just plain old shell command. You could use 'make' as well"`
PostCmd []string `toml:"post_cmd" usage:"Array of commands to run after ^C"`
Bin string `toml:"bin" usage:"Binary file yields from 'cmd', will be deprecated soon, recommend using entrypoint."`
Entrypoint entrypoint `toml:"entrypoint" usage:"Binary file plus optional arguments relative to root, prefer [\"./tmp/main\", \"arg\"] form"`
FullBin string `toml:"full_bin" usage:"Customize binary, can setup environment variables when run your app"`
ArgsBin []string `toml:"args_bin" usage:"Add additional arguments when running binary (bin/full_bin)."`
Log string `toml:"log" usage:"This log file is placed in your tmp_dir"`
IncludeExt []string `toml:"include_ext" usage:"Watch these filename extensions"`
ExcludeDir []string `toml:"exclude_dir" usage:"Ignore these filename extensions or directories"`
IncludeDir []string `toml:"include_dir" usage:"Watch these directories if you specified"`
ExcludeFile []string `toml:"exclude_file" usage:"Exclude files"`
IncludeFile []string `toml:"include_file" usage:"Watch these files"`
ExcludeRegex []string `toml:"exclude_regex" usage:"Exclude specific regular expressions"`
ExcludeUnchanged bool `toml:"exclude_unchanged" usage:"Exclude unchanged files"`
FollowSymlink bool `toml:"follow_symlink" usage:"Follow symlink for directories"`
Poll bool `toml:"poll" usage:"Poll files for changes instead of using fsnotify"`
PollInterval int `toml:"poll_interval" usage:"Poll interval (defaults to the minimum interval of 500ms)"`
Delay int `toml:"delay" usage:"It's not necessary to trigger build each time file changes if it's too frequent"`
StopOnError bool `toml:"stop_on_error" usage:"Stop running old binary when build errors occur"`
SendInterrupt bool `toml:"send_interrupt" usage:"Send Interrupt signal before killing process (windows does not support this feature)"`
KillDelay time.Duration `toml:"kill_delay" usage:"Delay after sending Interrupt signal"`
Rerun bool `toml:"rerun" usage:"Rerun binary or not"`
RerunDelay int `toml:"rerun_delay" usage:"Delay after each execution"`
regexCompiled []*regexp.Regexp
includeDirAbs []string
extraIncludeDirs []string
PreCmd []string `toml:"pre_cmd" usage:"Array of commands to run before each build"`
Cmd string `toml:"cmd" usage:"Just plain old shell command. You could use 'make' as well"`
PostCmd []string `toml:"post_cmd" usage:"Array of commands to run after ^C"`
Bin string `toml:"bin" usage:"Binary file yields from 'cmd', will be deprecated soon, recommend using entrypoint."`
Entrypoint entrypoint `toml:"entrypoint" usage:"Binary file plus optional arguments relative to root, prefer [\"./tmp/main\", \"arg\"] form"`
FullBin string `toml:"full_bin" usage:"Customize binary, can setup environment variables when run your app"`
ArgsBin []string `toml:"args_bin" usage:"Add additional arguments when running binary (bin/full_bin)."`
Log string `toml:"log" usage:"This log file is placed in your tmp_dir"`
IncludeExt []string `toml:"include_ext" usage:"Watch these filename extensions"`
ExcludeDir []string `toml:"exclude_dir" usage:"Ignore these filename extensions or directories"`
IncludeDir []string `toml:"include_dir" usage:"Watch these directories if you specified"`
ExcludeFile []string `toml:"exclude_file" usage:"Exclude files"`
IncludeFile []string `toml:"include_file" usage:"Watch these files"`
ExcludeRegex []string `toml:"exclude_regex" usage:"Exclude specific regular expressions"`
ExcludeUnchanged bool `toml:"exclude_unchanged" usage:"Exclude unchanged files"`
IgnoreDangerousRootDir bool `toml:"ignore_dangerous_root_dir" usage:"Ignore dangerous root directory that could cause excessive file watching"`
FollowSymlink bool `toml:"follow_symlink" usage:"Follow symlink for directories"`
Poll bool `toml:"poll" usage:"Poll files for changes instead of using fsnotify"`
PollInterval int `toml:"poll_interval" usage:"Poll interval (defaults to the minimum interval of 500ms)"`
Delay int `toml:"delay" usage:"It's not necessary to trigger build each time file changes if it's too frequent"`
StopOnError bool `toml:"stop_on_error" usage:"Stop running old binary when build errors occur"`
SendInterrupt bool `toml:"send_interrupt" usage:"Send Interrupt signal before killing process (windows does not support this feature)"`
KillDelay time.Duration `toml:"kill_delay" usage:"Delay after sending Interrupt signal"`
Rerun bool `toml:"rerun" usage:"Rerun binary or not"`
RerunDelay int `toml:"rerun_delay" usage:"Delay after each execution"`
regexCompiled []*regexp.Regexp
includeDirAbs []string
extraIncludeDirs []string
}
func (c *cfgBuild) RegexCompiled() ([]*regexp.Regexp, error) {
@@ -377,7 +378,10 @@ func (c *Config) preprocess(args map[string]TomlInfo) error {
// Check for dangerous root directories that could cause excessive file watching
if isDangerous, dirName := isDangerousRoot(c.Root); isDangerous {
return fmt.Errorf("refusing to run in %s - this would watch too many files. Please run air in a project directory", dirName)
if !c.Build.IgnoreDangerousRootDir {
return fmt.Errorf("refusing to run in %s - this would watch too many files. Please run air in a project directory", dirName)
}
fmt.Fprintln(os.Stdout, "[warning] ignoring root directory protections. This could cause excessive file watching. It is recommended to run air in a project directory")
}
if c.TmpDir == "" {
+116
View File
@@ -347,3 +347,119 @@ cmd = "go build -o ./tmp/main ."
t.Fatalf("missing bin deprecation warning in output: %q", output)
}
}
func TestWarnIgnoreDangerousRootDirProtection(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("HOME", tmpDir)
t.Run("when ignore_dangerous_root_dir is true", func(t *testing.T) {
cfgPath := filepath.Join(tmpDir, ".air.toml")
cfgContent := `
root = "/"
[build]
entrypoint = "tmp/main"
cmd = "go build -o ./tmp/main ."
ignore_dangerous_root_dir = true
`
if err := os.WriteFile(cfgPath, []byte(cfgContent), 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
oldStdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create pipe: %v", err)
}
os.Stdout = w
_, _ = InitConfig(cfgPath, nil)
if err := w.Close(); err != nil {
t.Fatalf("failed to close writer: %v", err)
}
os.Stdout = oldStdout
out, err := io.ReadAll(r)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
output := string(out)
if !strings.Contains(output, "ignoring root directory protections. This could cause excessive file watching. It is recommended to run air in a project directory") {
t.Fatalf("missing root directory protection warning in output: %q", output)
}
})
t.Run("when ignore_dangerous_root_dir is false", func(t *testing.T) {
cfgPath := filepath.Join(tmpDir, ".air.toml")
cfgContent := `
root = "/"
[build]
entrypoint = "tmp/main"
cmd = "go build -o ./tmp/main ."
ignore_dangerous_root_dir = false
`
if err := os.WriteFile(cfgPath, []byte(cfgContent), 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
oldStdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create pipe: %v", err)
}
os.Stdout = w
_, _ = InitConfig(cfgPath, nil)
if err := w.Close(); err != nil {
t.Fatalf("failed to close writer: %v", err)
}
os.Stdout = oldStdout
out, err := io.ReadAll(r)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
output := string(out)
if strings.Contains(output, "ignoring root directory protections") {
t.Fatalf("unexpected root directory protection warning in output: %q", output)
}
})
t.Run("when ignore_dangerous_root_dir is not set", func(t *testing.T) {
cfgPath := filepath.Join(tmpDir, ".air.toml")
cfgContent := `
root = "/"
[build]
entrypoint = "tmp/main"
cmd = "go build -o ./tmp/main ."
`
if err := os.WriteFile(cfgPath, []byte(cfgContent), 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
oldStdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create pipe: %v", err)
}
os.Stdout = w
_, _ = InitConfig(cfgPath, nil)
if err := w.Close(); err != nil {
t.Fatalf("failed to close writer: %v", err)
}
os.Stdout = oldStdout
out, err := io.ReadAll(r)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
output := string(out)
if strings.Contains(output, "ignoring root directory protections") {
t.Fatalf("unexpected root directory protection warning in output: %q", output)
}
})
}