diff --git a/tools/utils/atomic-write.go b/tools/utils/atomic-write.go index 1b9402f37..04681e011 100644 --- a/tools/utils/atomic-write.go +++ b/tools/utils/atomic-write.go @@ -12,6 +12,30 @@ import ( var _ = fmt.Print +func AtomicCreateSymlink(oldname, newname string) (err error) { + err = os.Symlink(oldname, newname) + if err == nil { + return nil + } + if !errors.Is(err, fs.ErrExist) { + return err + } + for { + tempname := newname + RandomFilename() + err = os.Symlink(oldname, tempname) + if err == nil { + err = os.Rename(tempname, newname) + if err != nil { + os.Remove(tempname) + } + return err + } + if !errors.Is(err, fs.ErrExist) { + return err + } + } +} + func AtomicWriteFile(path string, data []byte, perm os.FileMode) (err error) { npath, err := filepath.EvalSymlinks(path) if errors.Is(err, fs.ErrNotExist) { diff --git a/tools/utils/paths.go b/tools/utils/paths.go index 7e65281c2..dcebf9e58 100644 --- a/tools/utils/paths.go +++ b/tools/utils/paths.go @@ -3,11 +3,17 @@ package utils import ( + "crypto/rand" + "encoding/base32" + "fmt" "io/fs" + not_rand "math/rand" "os" + "os/exec" "os/user" "path/filepath" "runtime" + "strconv" "strings" "sync" @@ -57,9 +63,9 @@ func Abspath(path string) string { return path } -var config_dir, kitty_exe, cache_dir string +var config_dir, kitty_exe, cache_dir, runtime_dir string var kitty_exe_err error -var config_dir_once, kitty_exe_once, cache_dir_once sync.Once +var config_dir_once, kitty_exe_once, cache_dir_once, runtime_dir_once sync.Once func find_kitty_exe() { exe, err := os.Executable() @@ -133,6 +139,60 @@ func CacheDir() string { return cache_dir } +func macos_user_cache_dir() string { + // Sadly Go does not provide confstr() so we use this hack. We could + // Note that given a user generateduid and uid we can derive this by using + // the algorithm at https://github.com/ydkhatri/MacForensics/blob/master/darwin_path_generator.py + // but I cant find a good way to get the generateduid. Requires calling dscl in which case we might as well call getconf + // The data is in /var/db/dslocal/nodes/Default/users/.plist but it needs root + matches, err := filepath.Glob("/private/var/folders/*/*/C") + if err == nil { + for _, m := range matches { + s, err := os.Stat(m) + if err == nil { + if stat, ok := s.Sys().(unix.Stat_t); ok && s.IsDir() && int(stat.Uid) == os.Geteuid() && s.Mode().Perm() == 0o700 && unix.Access(m, unix.X_OK|unix.W_OK|unix.R_OK) == nil { + return m + } + } + } + } + out, err := exec.Command("/usr/bin/getconf", "DARWIN_USER_CACHE_DIR").Output() + if err == nil { + return strings.TrimSpace(UnsafeBytesToString(out)) + } + return "" +} + +func find_runtime_dir() { + var candidate string + if q := os.Getenv("KITTY_RUNTIME_DIRECTORY"); q != "" { + candidate = q + } else if runtime.GOOS == "darwin" { + candidate = macos_user_cache_dir() + } else if q := os.Getenv("XDG_RUNTIME_DIR"); q != "" { + candidate = q + } + candidate = strings.TrimRight(candidate, "/") + if candidate == "" { + q := fmt.Sprintf("/run/user/%d", os.Geteuid()) + if s, err := os.Stat(q); err == nil && s.IsDir() && unix.Access(q, unix.X_OK|unix.R_OK|unix.W_OK) == nil { + candidate = q + } else { + candidate = filepath.Join(CacheDir(), "run") + } + } + os.MkdirAll(candidate, 0o700) + if s, err := os.Stat(candidate); err == nil && s.Mode().Perm() != 0o700 { + os.Chmod(candidate, 0o700) + } + runtime_dir = candidate +} + +func RuntimeDir() string { + runtime_dir_once.Do(find_runtime_dir) + return runtime_dir +} + type Walk_callback func(path, abspath string, d fs.DirEntry, err error) error func transform_symlink(path string) string { @@ -205,3 +265,13 @@ func WalkWithSymlink(dirpath string, callback Walk_callback, transformers ...fun seen: make(map[string]bool), real_callback: callback, transform_func: transform, needs_recurse_func: needs_symlink_recurse} return sw.walk(dirpath) } + +func RandomFilename() string { + b := []byte{0, 0, 0, 0, 0, 0, 0, 0} + _, err := rand.Read(b) + if err != nil { + return strconv.FormatUint(uint64(not_rand.Uint32()), 16) + } + return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b) + +} diff --git a/tools/utils/shm/shm.go b/tools/utils/shm/shm.go index 09f7698a4..389b21f4e 100644 --- a/tools/utils/shm/shm.go +++ b/tools/utils/shm/shm.go @@ -3,16 +3,12 @@ package shm import ( - "crypto/rand" - "encoding/base32" "encoding/binary" "errors" "fmt" "io" "io/fs" - not_rand "math/rand" "os" - "strconv" "strings" "kitty/tools/cli" @@ -48,15 +44,6 @@ func prefix_and_suffix(pattern string) (prefix, suffix string, err error) { return prefix, suffix, nil } -func next_random() string { - b := make([]byte, 8) - _, err := rand.Read(b) - if err != nil { - return strconv.FormatUint(uint64(not_rand.Uint32()), 16) - } - return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b) -} - type MMap interface { Close() error Unlink() error diff --git a/tools/utils/shm/shm_fs.go b/tools/utils/shm/shm_fs.go index 4ca5c8e6c..5722665f5 100644 --- a/tools/utils/shm/shm_fs.go +++ b/tools/utils/shm/shm_fs.go @@ -8,10 +8,11 @@ import ( "errors" "fmt" "io/fs" - "kitty/tools/utils" "os" "path/filepath" "runtime" + + "kitty/tools/utils" ) var _ = fmt.Print @@ -96,7 +97,7 @@ func create_temp(pattern string, size uint64) (ans MMap, err error) { var f *os.File try := 0 for { - name := prefix + next_random() + suffix + name := prefix + utils.RandomFilename() + suffix path := file_path_from_name(name) f, err = os.OpenFile(path, os.O_EXCL|os.O_CREATE|os.O_RDWR, 0600) if err != nil { diff --git a/tools/utils/shm/shm_syscall.go b/tools/utils/shm/shm_syscall.go index 901a34087..f4a8f2956 100644 --- a/tools/utils/shm/shm_syscall.go +++ b/tools/utils/shm/shm_syscall.go @@ -11,6 +11,8 @@ import ( "strings" "unsafe" + "kitty/tools/utils" + "golang.org/x/sys/unix" ) @@ -127,7 +129,7 @@ func create_temp(pattern string, size uint64) (ans MMap, err error) { var f *os.File try := 0 for { - name := prefix + next_random() + suffix + name := prefix + utils.RandomFilename() + suffix if len(name) > SHM_NAME_MAX { return nil, ErrPatternTooLong }