+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "syscall"
+)
+
+// tokenLockFileName is the lock file of the token file in the data directory.
+const tokenLockFileName = "auth.lock"
+
+// errTokensInUse tells that another onechat process uses the token file.
+var errTokensInUse = errors.New("another onechat process uses the token file, such as the daemon")
+
+// errLocked tells that another process owns the chat.
+var errLocked = errors.New("another process owns the chat")
+
+// lockTokens takes the lock of the token file in dir. Each process that reads
+// or refreshes the tokens holds it, because a refresh in one process rotates
+// the refresh token of the other and logs it out.
+func lockTokens(dir string) (*os.File, error) {
+ lock, err := lockFile(filepath.Join(dir, tokenLockFileName))
+ if errors.Is(err, errLocked) {
+ return nil, errTokensInUse
+ }
+
+ return lock, err
+}
+
+// lockFile opens the file at path and takes an exclusive lock on it. The lock
+// stays until the file closes or the process ends.
+func lockFile(path string) (*os.File, error) {
+ file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
+ if err != nil {
+ return nil, fmt.Errorf("open lock file: %w", err)
+ }
+
+ err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
+ if errors.Is(err, syscall.EWOULDBLOCK) {
+ err = errLocked
+ }
+
+ if err != nil {
+ return nil, errors.Join(fmt.Errorf("lock chat: %w", err), file.Close())
+ }
+
+ return file, nil
+}