]> git.giacomolli.uk - onechat/blob - lock.go
Render Markdown with plain terminal styles
[onechat] / lock.go
1 package main
2
3 import (
4         "errors"
5         "fmt"
6         "os"
7         "path/filepath"
8         "syscall"
9 )
10
11 // tokenLockFileName is the lock file of the token file in the data directory.
12 const tokenLockFileName = "auth.lock"
13
14 // errTokensInUse tells that another onechat process uses the token file.
15 var errTokensInUse = errors.New("another onechat process uses the token file, such as the daemon")
16
17 // errLocked tells that another process owns the chat.
18 var errLocked = errors.New("another process owns the chat")
19
20 // lockTokens takes the lock of the token file in dir. Each process that reads
21 // or refreshes the tokens holds it, because a refresh in one process rotates
22 // the refresh token of the other and logs it out.
23 func lockTokens(dir string) (*os.File, error) {
24         lock, err := lockFile(filepath.Join(dir, tokenLockFileName))
25         if errors.Is(err, errLocked) {
26                 return nil, errTokensInUse
27         }
28
29         return lock, err
30 }
31
32 // lockFile opens the file at path and takes an exclusive lock on it. The lock
33 // stays until the file closes or the process ends.
34 func lockFile(path string) (*os.File, error) {
35         file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
36         if err != nil {
37                 return nil, fmt.Errorf("open lock file: %w", err)
38         }
39
40         err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
41         if errors.Is(err, syscall.EWOULDBLOCK) {
42                 err = errLocked
43         }
44
45         if err != nil {
46                 return nil, errors.Join(fmt.Errorf("lock chat: %w", err), file.Close())
47         }
48
49         return file, nil
50 }