• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

happy-sdk / happy / 15648782655

14 Jun 2025 05:18AM UTC coverage: 49.732% (-0.08%) from 49.808%
15648782655

push

github

mkungla
feat: auto remove stale pid file

Signed-off-by: Marko Kungla <marko.kungla@gmail.com>

8 of 41 new or added lines in 1 file covered. (19.51%)

8065 of 16217 relevant lines covered (49.73%)

102260.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.42
/sdk/instance/instance.go
1
// SPDX-License-Identifier: Apache-2.0
2
//
3
// Copyright © 2022 The Happy Authors
4

5
package instance
6

7
import (
8
        "bytes"
9
        "crypto/sha1"
10
        "encoding/hex"
11
        "errors"
12
        "fmt"
13
        "log/slog"
14
        "os"
15
        "path/filepath"
16
        "strconv"
17
        "syscall"
18
        "time"
19

20
        "github.com/happy-sdk/happy/pkg/settings"
21
        "github.com/happy-sdk/happy/sdk/app/session"
22
        "github.com/happy-sdk/happy/sdk/internal"
23
)
24

25
type Settings struct {
26
        // How many instances of the applications can be booted at the same time.
27
        Max settings.Uint `key:"max" default:"1" desc:"Maximum number of instances of the application that can be booted at the same time"`
28
}
29

30
func (s Settings) Blueprint() (*settings.Blueprint, error) {
×
31
        b, err := settings.New(s)
×
32
        if err != nil {
×
33
                return nil, err
×
34
        }
×
35

36
        return b, nil
×
37
}
38

39
type Instance struct {
40
        id      ID
41
        sess    *session.Context
42
        pidfile string
43
}
44

45
var Error = errors.New("instance error")
46

47
type ID string
48

49
func NewID() ID {
12✔
50
        hasher := sha1.New()
12✔
51
        _, _ = fmt.Fprint(hasher, time.Now().UnixMilli())
12✔
52
        hashSum := hasher.Sum(nil)
12✔
53
        fullID := hex.EncodeToString(hashSum)
12✔
54
        return ID(fullID[:8])
12✔
55
}
12✔
56

57
func (id ID) String() string {
42✔
58
        return string(id)
42✔
59
}
42✔
60

61
// New creates a new instance for the application.
62
func New(sess *session.Context) (*Instance, error) {
9✔
63
        if sess == nil {
9✔
64
                return nil, fmt.Errorf("%w: session is nil", Error)
×
65
        }
×
66

67
        pidsdir := sess.Opts().Get("app.fs.path.pids").String()
9✔
68
        if _, err := os.Stat(pidsdir); err != nil {
9✔
69
                return nil, fmt.Errorf("%w: pids directory not found: %s", Error, pidsdir)
×
70
        }
×
71

72
        pidfiles, err := verifyPidFiles(sess, pidsdir)
9✔
73
        if err != nil {
9✔
NEW
74
                return nil, fmt.Errorf("%w: failed to read pids directory: %s", Error, err.Error())
×
75
        }
×
76

77
        inst := &Instance{
9✔
78
                id:   ID(sess.Opts().Get("app.instance.id").String()),
9✔
79
                sess: sess,
9✔
80
        }
9✔
81

9✔
82
        if len(pidfiles) >= sess.Settings().Get("app.instance.max").Value().Int() {
9✔
83
                sess.Log().Error("existing pid files")
×
84
                for _, pidfile := range pidfiles {
×
85
                        sess.Log().Println(filepath.Join(pidsdir, pidfile.Name()))
×
86
                }
×
87
                return nil, fmt.Errorf("%w: max instances reached (%s)", Error, sess.Settings().Get("app.instance.max").String())
×
88
        }
89

90
        inst.pidfile = filepath.Join(
9✔
91
                pidsdir,
9✔
92
                fmt.Sprintf("instance-%s.pid", inst.id.String()),
9✔
93
        )
9✔
94
        internal.Log(sess.Log(), "create pid lock file", slog.String("file", inst.pidfile))
9✔
95

9✔
96
        if err := os.WriteFile(inst.pidfile, []byte(inst.sess.Opts().Get("app.pid").String()), 0644); err != nil {
9✔
97
                return nil, fmt.Errorf("%w: failed to write intance PID file: %s", Error, err.Error())
×
98
        }
×
99

100
        return inst, nil
9✔
101
}
102

103
func (inst *Instance) Dispose() error {
9✔
104
        internal.Log(inst.sess.Log(), "disposing instance", slog.String("id", inst.id.String()))
9✔
105
        // delete the pidfile
9✔
106
        if _, err := os.Stat(inst.pidfile); err == nil {
9✔
107
                if err := os.Remove(inst.pidfile); err != nil {
×
108
                        return fmt.Errorf("failed to delete pidfile %s: %w", inst.pidfile, err)
×
109
                }
×
110
                if inst.sess != nil {
×
111
                        internal.Log(inst.sess.Log(), "successfully deleted pidfile", slog.String("pidfile", inst.pidfile))
×
112
                }
×
113
        }
114
        return nil
9✔
115
}
116

117
func verifyPidFiles(sess *session.Context, pidsdir string) ([]os.DirEntry, error) {
9✔
118

9✔
119
        pidfiles, err := os.ReadDir(pidsdir)
9✔
120
        if err != nil {
9✔
NEW
121
                return nil, err
×
NEW
122
        }
×
123

124
        var res []os.DirEntry
9✔
125
        for _, pidfile := range pidfiles {
9✔
NEW
126
                if pidfile.IsDir() {
×
NEW
127
                        continue
×
128
                }
NEW
129
                if ok, err := verifyPidFile(sess, pidfile); err != nil {
×
NEW
130
                        return nil, err
×
NEW
131
                } else if ok {
×
NEW
132
                        res = append(res, pidfile)
×
NEW
133
                }
×
134
        }
135
        return res, nil
9✔
136
}
137

NEW
138
func verifyPidFile(sess *session.Context, pidfile os.DirEntry) (ok bool, err error) {
×
NEW
139
        pidfileAbs := filepath.Join(sess.Opts().Get("app.fs.path.pids").String(), pidfile.Name())
×
NEW
140
        data, err := os.ReadFile(pidfileAbs)
×
NEW
141
        if err != nil {
×
NEW
142
                return false, err
×
NEW
143
        }
×
NEW
144
        pid := string(bytes.TrimSpace(data))
×
NEW
145
        if pid == "" {
×
NEW
146
                return false, nil
×
NEW
147
        }
×
NEW
148
        pidInt, err := strconv.Atoi(pid)
×
NEW
149
        if err != nil {
×
NEW
150
                return false, err
×
NEW
151
        }
×
152

NEW
153
        p, err := os.FindProcess(pidInt)
×
NEW
154
        if err != nil {
×
NEW
155
                return false, err
×
NEW
156
        }
×
NEW
157
        if err := p.Signal(syscall.Signal(0)); err != nil {
×
NEW
158
                sess.Log().Notice(fmt.Sprintf("previous process %d: %s", pidInt, err.Error()))
×
NEW
159
                return false, os.Remove(pidfileAbs)
×
NEW
160
        }
×
NEW
161
        return true, nil
×
162
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc