sensorium-mcp 3.0.4 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/dashboard/routes/data.d.ts.map +1 -1
  2. package/dist/dashboard/routes/data.js +2 -1
  3. package/dist/dashboard/routes/data.js.map +1 -1
  4. package/dist/dashboard/routes/threads.js +1 -1
  5. package/dist/dashboard/routes/threads.js.map +1 -1
  6. package/dist/dashboard/routes.d.ts.map +1 -1
  7. package/dist/dashboard/routes.js +1 -3
  8. package/dist/dashboard/routes.js.map +1 -1
  9. package/dist/data/memory/migration-runner.d.ts +1 -1
  10. package/dist/data/memory/migration-runner.d.ts.map +1 -1
  11. package/dist/data/memory/migration-runner.js +59 -3
  12. package/dist/data/memory/migration-runner.js.map +1 -1
  13. package/dist/data/memory/schema-ddl.d.ts +1 -1
  14. package/dist/data/memory/schema-ddl.d.ts.map +1 -1
  15. package/dist/data/memory/schema-ddl.js +2 -1
  16. package/dist/data/memory/schema-ddl.js.map +1 -1
  17. package/dist/data/memory/thread-registry.js +1 -1
  18. package/dist/data/memory/thread-registry.js.map +1 -1
  19. package/dist/http-server.d.ts.map +1 -1
  20. package/dist/http-server.js +1 -9
  21. package/dist/http-server.js.map +1 -1
  22. package/dist/index.js +3 -6
  23. package/dist/index.js.map +1 -1
  24. package/dist/server/factory.js +1 -1
  25. package/dist/server/factory.js.map +1 -1
  26. package/dist/services/agent-spawn.service.d.ts +7 -1
  27. package/dist/services/agent-spawn.service.d.ts.map +1 -1
  28. package/dist/services/agent-spawn.service.js +69 -45
  29. package/dist/services/agent-spawn.service.js.map +1 -1
  30. package/dist/services/consolidation.service.d.ts.map +1 -1
  31. package/dist/services/consolidation.service.js +49 -35
  32. package/dist/services/consolidation.service.js.map +1 -1
  33. package/dist/services/keeper.service.d.ts +21 -0
  34. package/dist/services/keeper.service.d.ts.map +1 -0
  35. package/dist/services/keeper.service.js +195 -0
  36. package/dist/services/keeper.service.js.map +1 -0
  37. package/dist/services/maintenance-signal.d.ts +2 -0
  38. package/dist/services/maintenance-signal.d.ts.map +1 -1
  39. package/dist/services/maintenance-signal.js +7 -1
  40. package/dist/services/maintenance-signal.js.map +1 -1
  41. package/dist/services/process.service.d.ts +19 -2
  42. package/dist/services/process.service.d.ts.map +1 -1
  43. package/dist/services/process.service.js +104 -10
  44. package/dist/services/process.service.js.map +1 -1
  45. package/dist/services/thread-lifecycle.service.d.ts +5 -0
  46. package/dist/services/thread-lifecycle.service.d.ts.map +1 -1
  47. package/dist/services/thread-lifecycle.service.js +33 -8
  48. package/dist/services/thread-lifecycle.service.js.map +1 -1
  49. package/dist/services/worker-cleanup.service.d.ts +14 -1
  50. package/dist/services/worker-cleanup.service.d.ts.map +1 -1
  51. package/dist/services/worker-cleanup.service.js +36 -38
  52. package/dist/services/worker-cleanup.service.js.map +1 -1
  53. package/dist/sessions.d.ts +0 -5
  54. package/dist/sessions.d.ts.map +1 -1
  55. package/dist/sessions.js +0 -7
  56. package/dist/sessions.js.map +1 -1
  57. package/dist/stdio-server.d.ts.map +1 -1
  58. package/dist/stdio-server.js +1 -7
  59. package/dist/stdio-server.js.map +1 -1
  60. package/dist/tools/delegate-tool.d.ts.map +1 -1
  61. package/dist/tools/delegate-tool.js +2 -2
  62. package/dist/tools/delegate-tool.js.map +1 -1
  63. package/dist/tools/session-tools.js +1 -1
  64. package/dist/tools/session-tools.js.map +1 -1
  65. package/dist/tools/start-session-tool.d.ts.map +1 -1
  66. package/dist/tools/start-session-tool.js +8 -9
  67. package/dist/tools/start-session-tool.js.map +1 -1
  68. package/dist/tools/wait/message-processing.d.ts.map +1 -1
  69. package/dist/tools/wait/message-processing.js +28 -0
  70. package/dist/tools/wait/message-processing.js.map +1 -1
  71. package/dist/tools/wait/poll-loop.js +1 -1
  72. package/dist/tools/wait/poll-loop.js.map +1 -1
  73. package/package.json +1 -1
  74. package/dist/tools/thread-lifecycle.d.ts +0 -6
  75. package/dist/tools/thread-lifecycle.d.ts.map +0 -1
  76. package/dist/tools/thread-lifecycle.js +0 -6
  77. package/dist/tools/thread-lifecycle.js.map +0 -1
  78. package/supervisor/config.go +0 -253
  79. package/supervisor/config_test.go +0 -78
  80. package/supervisor/go.mod +0 -15
  81. package/supervisor/go.sum +0 -20
  82. package/supervisor/health.go +0 -433
  83. package/supervisor/health_test.go +0 -93
  84. package/supervisor/keeper.go +0 -309
  85. package/supervisor/keeper_test.go +0 -27
  86. package/supervisor/lock.go +0 -57
  87. package/supervisor/lock_test.go +0 -54
  88. package/supervisor/log.go +0 -195
  89. package/supervisor/log_test.go +0 -125
  90. package/supervisor/main.go +0 -475
  91. package/supervisor/main_test.go +0 -130
  92. package/supervisor/notify.go +0 -53
  93. package/supervisor/process.go +0 -294
  94. package/supervisor/process_test.go +0 -108
  95. package/supervisor/process_unix.go +0 -14
  96. package/supervisor/process_windows.go +0 -15
  97. package/supervisor/secrets.go +0 -95
  98. package/supervisor/secrets_securevault_test.go +0 -98
  99. package/supervisor/secrets_test.go +0 -119
  100. package/supervisor/self_update.go +0 -282
  101. package/supervisor/self_update_test.go +0 -177
  102. package/supervisor/service_restart_stub.go +0 -9
  103. package/supervisor/service_restart_windows.go +0 -63
  104. package/supervisor/service_stub.go +0 -15
  105. package/supervisor/service_windows.go +0 -194
  106. package/supervisor/update_state.go +0 -264
  107. package/supervisor/update_state_test.go +0 -306
  108. package/supervisor/updater.go +0 -613
  109. package/supervisor/updater_test.go +0 -64
@@ -1,130 +0,0 @@
1
- package main
2
-
3
- import (
4
- "os"
5
- "path/filepath"
6
- "strings"
7
- "testing"
8
- )
9
-
10
- func TestResolveRunSupervisorMode(t *testing.T) {
11
- tests := []struct {
12
- name string
13
- processIsService bool
14
- hostMode string
15
- want bool
16
- }{
17
- {
18
- name: "service process always forces service mode",
19
- processIsService: true,
20
- hostMode: "task",
21
- want: true,
22
- },
23
- {
24
- name: "non-service defaults to task mode",
25
- processIsService: false,
26
- hostMode: "",
27
- want: false,
28
- },
29
- {
30
- name: "non-service task remains task mode",
31
- processIsService: false,
32
- hostMode: "task",
33
- want: false,
34
- },
35
- {
36
- name: "non-service service mode is honored",
37
- processIsService: false,
38
- hostMode: "service",
39
- want: true,
40
- },
41
- }
42
-
43
- for _, tc := range tests {
44
- t.Run(tc.name, func(t *testing.T) {
45
- got := resolveRunSupervisorMode(tc.processIsService, tc.hostMode)
46
- if got != tc.want {
47
- t.Fatalf("resolveRunSupervisorMode(processIsService=%v, hostMode=%q) = %v, want %v", tc.processIsService, tc.hostMode, got, tc.want)
48
- }
49
- })
50
- }
51
- }
52
-
53
- func TestRunSupervisor_DoesNotRecoverPersistedStateWhenWatcherLockNotAcquired(t *testing.T) {
54
- tempHome := t.TempDir()
55
- t.Setenv("USERPROFILE", tempHome)
56
- t.Setenv("HOME", tempHome)
57
- t.Setenv("MCP_HTTP_PORT", "7777")
58
- t.Setenv("MCP_HTTP_SECRET", "test-secret")
59
- t.Setenv("TELEGRAM_TOKEN", "test-token")
60
- t.Setenv("TELEGRAM_CHAT_ID", "test-chat")
61
-
62
- dataDir := filepath.Join(tempHome, ".remote-copilot-mcp")
63
- log := NewLogger(filepath.Join(dataDir, "test.log"))
64
- defer log.Close()
65
-
66
- store := NewUpdateStateStore(filepath.Join(dataDir, "update-state.json"), log)
67
- store.Transition(updateScopeMCP, updatePhaseApplying, "2.0.0", "1.0.0", "")
68
-
69
- watcherLock := filepath.Join(dataDir, "watcher.lock")
70
- if !AcquireLock(watcherLock, log) {
71
- t.Fatal("failed to pre-acquire watcher lock")
72
- }
73
- defer ReleaseLock(watcherLock)
74
-
75
- err := runSupervisor(false)
76
- if err == nil {
77
- t.Fatal("expected runSupervisor to fail when watcher lock is already held")
78
- }
79
- if !strings.Contains(err.Error(), "another supervisor instance") {
80
- t.Fatalf("unexpected error: %v", err)
81
- }
82
-
83
- state, loadErr := store.Load()
84
- if loadErr != nil {
85
- t.Fatalf("failed to load update state: %v", loadErr)
86
- }
87
- if state.Phase != updatePhaseApplying {
88
- t.Fatalf("phase = %q, want %q", state.Phase, updatePhaseApplying)
89
- }
90
- }
91
-
92
- func TestRunSupervisor_DoesNotApplyPendingSupervisorUpdateWhenWatcherLockNotAcquired(t *testing.T) {
93
- tempHome := t.TempDir()
94
- t.Setenv("USERPROFILE", tempHome)
95
- t.Setenv("HOME", tempHome)
96
- t.Setenv("MCP_HTTP_PORT", "7777")
97
- t.Setenv("MCP_HTTP_SECRET", "test-secret")
98
- t.Setenv("TELEGRAM_TOKEN", "test-token")
99
- t.Setenv("TELEGRAM_CHAT_ID", "test-chat")
100
-
101
- dataDir := filepath.Join(tempHome, ".remote-copilot-mcp")
102
- log := NewLogger(filepath.Join(dataDir, "test.log"))
103
- defer log.Close()
104
-
105
- pendingVersion := filepath.Join(dataDir, "bin", "sensorium-supervisor.new.exe.version")
106
- if err := os.MkdirAll(filepath.Dir(pendingVersion), 0755); err != nil {
107
- t.Fatalf("failed to create pending version directory: %v", err)
108
- }
109
- if err := os.WriteFile(pendingVersion, []byte("2.0.0"), 0644); err != nil {
110
- t.Fatalf("failed to create stale pending version file: %v", err)
111
- }
112
-
113
- watcherLock := filepath.Join(dataDir, "watcher.lock")
114
- if !AcquireLock(watcherLock, log) {
115
- t.Fatal("failed to pre-acquire watcher lock")
116
- }
117
- defer ReleaseLock(watcherLock)
118
-
119
- err := runSupervisor(false)
120
- if err == nil {
121
- t.Fatal("expected runSupervisor to fail when watcher lock is already held")
122
- }
123
- if !strings.Contains(err.Error(), "another supervisor instance") {
124
- t.Fatalf("unexpected error: %v", err)
125
- }
126
-
127
- if _, statErr := os.Stat(pendingVersion); statErr != nil {
128
- t.Fatalf("pending supervisor version file was unexpectedly modified: %v", statErr)
129
- }
130
- }
@@ -1,53 +0,0 @@
1
- package main
2
-
3
- import (
4
- "context"
5
- "fmt"
6
- "net/http"
7
- "net/url"
8
- "strings"
9
- "time"
10
- )
11
-
12
- // NotifyOperator sends a message via Telegram to the operator.
13
- // Silently fails if credentials are not configured.
14
- func NotifyOperator(cfg Config, log *Logger, text string, threadID int) {
15
- if cfg.TelegramToken == "" || cfg.TelegramChatID == "" {
16
- log.Debug("NotifyOperator: skipped (no Telegram credentials)")
17
- return
18
- }
19
-
20
- log.Debug("NotifyOperator: sending to chat %s (threadID=%d)", cfg.TelegramChatID, threadID)
21
-
22
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
23
- defer cancel()
24
-
25
- apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", cfg.TelegramToken)
26
-
27
- form := url.Values{}
28
- form.Set("chat_id", cfg.TelegramChatID)
29
- form.Set("text", text)
30
- form.Set("parse_mode", "HTML")
31
- if threadID > 0 {
32
- form.Set("message_thread_id", fmt.Sprintf("%d", threadID))
33
- }
34
-
35
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(form.Encode()))
36
- if err != nil {
37
- log.Warn("Telegram notify: %v", err)
38
- return
39
- }
40
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
41
-
42
- resp, err := http.DefaultClient.Do(req)
43
- if err != nil {
44
- log.Warn("Telegram notify: %v", err)
45
- return
46
- }
47
- defer resp.Body.Close()
48
- if resp.StatusCode >= 400 {
49
- log.Warn("Telegram notify: HTTP %d", resp.StatusCode)
50
- } else {
51
- log.Debug("Telegram notify: sent OK (HTTP %d)", resp.StatusCode)
52
- }
53
- }
@@ -1,294 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "errors"
6
- "fmt"
7
- "os"
8
- "os/exec"
9
- "path/filepath"
10
- "runtime"
11
- "strconv"
12
- "strings"
13
- "syscall"
14
- "time"
15
- )
16
-
17
- // SpawnMCPServer starts the MCP server as a detached child process.
18
- // Returns the PID of the spawned process.
19
- func SpawnMCPServer(cfg Config, log *Logger) (int, error) {
20
- parts := strings.Fields(cfg.MCPStartCommand)
21
- if len(parts) == 0 {
22
- return 0, errors.New("empty MCP_START_COMMAND")
23
- }
24
-
25
- if err := os.MkdirAll(filepath.Dir(cfg.Paths.MCPStderrLog), 0755); err != nil {
26
- return 0, fmt.Errorf("create MCP stderr log directory: %w", err)
27
- }
28
- stderrFile, err := os.OpenFile(cfg.Paths.MCPStderrLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
29
- if err != nil {
30
- return 0, fmt.Errorf("open MCP stderr log: %w", err)
31
- }
32
-
33
- cmd := exec.Command(parts[0], parts[1:]...)
34
- cmd.Env = os.Environ()
35
- for k, v := range cfg.ResolvedProfileEnv {
36
- if v == "" {
37
- continue
38
- }
39
- cmd.Env = upsertEnv(cmd.Env, k, v)
40
- }
41
- if cfg.MCPHttpPort > 0 {
42
- cmd.Env = upsertEnv(cmd.Env, "MCP_HTTP_PORT", strconv.Itoa(cfg.MCPHttpPort))
43
- }
44
- if cfg.MCPHttpSecret != "" {
45
- cmd.Env = upsertEnv(cmd.Env, "MCP_HTTP_SECRET", cfg.MCPHttpSecret)
46
- }
47
- if cfg.TelegramToken != "" {
48
- cmd.Env = upsertEnv(cmd.Env, "TELEGRAM_TOKEN", cfg.TelegramToken)
49
- }
50
- if cfg.TelegramChatID != "" {
51
- cmd.Env = upsertEnv(cmd.Env, "TELEGRAM_CHAT_ID", cfg.TelegramChatID)
52
- }
53
- cmd.Stdin = nil
54
- cmd.Stdout = nil
55
- cmd.Stderr = stderrFile
56
- setSysProcAttr(cmd)
57
-
58
- log.Info("Starting MCP server: %s", cfg.MCPStartCommand)
59
- log.Info("Capturing MCP server stderr to %s", cfg.Paths.MCPStderrLog)
60
-
61
- if err := cmd.Start(); err != nil {
62
- _ = stderrFile.Close()
63
- return 0, fmt.Errorf("spawn MCP server: %w", err)
64
- }
65
-
66
- pid := cmd.Process.Pid
67
- log.Info("MCP server started with PID %d", pid)
68
-
69
- // Don't wait — detached process
70
- go func() {
71
- _ = cmd.Wait()
72
- _ = stderrFile.Close()
73
- }()
74
-
75
- if err := writePIDFile(cfg.Paths.ServerPID, pid); err != nil {
76
- log.Warn("Failed to write server PID file: %v", err)
77
- }
78
-
79
- return pid, nil
80
- }
81
-
82
- func upsertEnv(env []string, key, value string) []string {
83
- prefix := key + "="
84
- for i, kv := range env {
85
- if strings.HasPrefix(kv, prefix) {
86
- env[i] = prefix + value
87
- return env
88
- }
89
- }
90
- return append(env, prefix+value)
91
- }
92
-
93
- // KillProcess kills a process by PID. On Windows, uses taskkill /F /T for tree kill.
94
- func KillProcess(pid int, log *Logger) error {
95
- if !IsProcessAlive(pid) {
96
- log.Debug("KillProcess: PID %d already dead", pid)
97
- return nil
98
- }
99
-
100
- log.Debug("KillProcess: killing PID %d", pid)
101
-
102
- if runtime.GOOS == "windows" {
103
- // taskkill /F /T kills the tree
104
- out, err := exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(pid)).CombinedOutput()
105
- if err != nil {
106
- return fmt.Errorf("taskkill PID %d: %w (%s)", pid, err, strings.TrimSpace(string(out)))
107
- }
108
- log.Info("Killed process tree PID %d", pid)
109
- return nil
110
- }
111
-
112
- // Unix: SIGTERM, wait 2s, then SIGKILL
113
- proc, err := os.FindProcess(pid)
114
- if err != nil {
115
- log.Debug("KillProcess: FindProcess(%d) failed: %v", pid, err)
116
- return err
117
- }
118
- if err := proc.Signal(syscall.SIGTERM); err != nil {
119
- // Already dead
120
- return nil
121
- }
122
- time.Sleep(2 * time.Second)
123
- if IsProcessAlive(pid) {
124
- _ = proc.Kill()
125
- log.Info("Force-killed PID %d", pid)
126
- } else {
127
- log.Info("Process PID %d terminated gracefully", pid)
128
- }
129
- return nil
130
- }
131
-
132
- // KillByPort finds a process listening on the given port and kills it (Windows-only orphan cleanup).
133
- func KillByPort(port int, log *Logger) {
134
- if runtime.GOOS != "windows" || port <= 0 || port > 65535 {
135
- return
136
- }
137
- log.Debug("KillByPort: checking for processes on port %d", port)
138
- out, err := exec.Command("cmd", "/c", fmt.Sprintf("netstat -aon | findstr \":%d.*LISTENING\"", port)).CombinedOutput()
139
- if err != nil {
140
- log.Debug("KillByPort: no listeners on port %d", port)
141
- return
142
- }
143
- for _, line := range strings.Split(string(out), "\n") {
144
- fields := strings.Fields(strings.TrimSpace(line))
145
- if len(fields) >= 5 {
146
- pid, err := strconv.Atoi(fields[len(fields)-1])
147
- if err == nil && pid > 0 {
148
- log.Info("Found orphan PID %d on port %d — killing", pid, port)
149
- _ = KillProcess(pid, log)
150
- }
151
- }
152
- }
153
- }
154
-
155
- // IsProcessAlive checks whether a process with the given PID exists.
156
- func IsProcessAlive(pid int) bool {
157
- if pid <= 0 {
158
- return false
159
- }
160
- if runtime.GOOS == "windows" {
161
- out, err := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid), "/NH").CombinedOutput()
162
- if err != nil {
163
- return false
164
- }
165
- return strings.Contains(string(out), strconv.Itoa(pid))
166
- }
167
- proc, err := os.FindProcess(pid)
168
- if err != nil {
169
- return false
170
- }
171
- return proc.Signal(syscall.Signal(0)) == nil // signal 0 = existence check on Unix
172
- }
173
-
174
- // --- PID File Helpers ---
175
-
176
- type pidJSON struct {
177
- PID int `json:"pid"`
178
- }
179
-
180
- // ReadPIDFile reads a PID from a file. Supports both JSON {"pid":123} and raw integer formats.
181
- func ReadPIDFile(path string) (int, error) {
182
- data, err := os.ReadFile(path)
183
- if err != nil {
184
- return 0, err
185
- }
186
- raw := strings.TrimSpace(string(data))
187
-
188
- // Try JSON first
189
- var pj pidJSON
190
- if json.Unmarshal([]byte(raw), &pj) == nil && pj.PID > 0 {
191
- return pj.PID, nil
192
- }
193
-
194
- // Fallback: raw integer
195
- pid, err := strconv.Atoi(raw)
196
- if err != nil {
197
- return 0, fmt.Errorf("invalid PID file content: %q", raw)
198
- }
199
- if pid <= 0 {
200
- return 0, fmt.Errorf("invalid PID: %d", pid)
201
- }
202
- return pid, nil
203
- }
204
-
205
- func writePIDFile(path string, pid int) error {
206
- dir := filepath.Dir(path)
207
- if err := os.MkdirAll(dir, 0755); err != nil {
208
- return err
209
- }
210
- data, _ := json.Marshal(pidJSON{PID: pid})
211
- return atomicWrite(path, data)
212
- }
213
-
214
- // atomicWrite writes data to a temp file then renames — prevents partial reads.
215
- func atomicWrite(path string, data []byte) error {
216
- tmp := fmt.Sprintf("%s.tmp.%d", path, os.Getpid())
217
- if err := os.WriteFile(tmp, data, 0644); err != nil {
218
- return err
219
- }
220
- return os.Rename(tmp, path)
221
- }
222
-
223
- // ListThreadPIDs returns a map of threadId → PID from the pids directory.
224
- func ListThreadPIDs(pidsDir string) map[string]int {
225
- result := make(map[string]int)
226
- entries, err := os.ReadDir(pidsDir)
227
- if err != nil {
228
- // directory may not exist yet
229
- return result
230
- }
231
- for _, e := range entries {
232
- if e.IsDir() || !strings.HasSuffix(e.Name(), ".pid") {
233
- continue
234
- }
235
- threadID := strings.TrimSuffix(e.Name(), ".pid")
236
- pid, err := ReadPIDFile(filepath.Join(pidsDir, e.Name()))
237
- if err != nil {
238
- continue
239
- }
240
- result[threadID] = pid
241
- }
242
- return result
243
- }
244
-
245
- // KillOrphanThreads kills any alive processes listed in PID files (leftovers from
246
- // a previous supervisor run) and then removes the PID files. Called on startup
247
- // before the MCP server is launched, so all thread PIDs are guaranteed to be stale.
248
- func KillOrphanThreads(pidsDir string, log *Logger) {
249
- pids := ListThreadPIDs(pidsDir)
250
- if len(pids) == 0 {
251
- log.Debug("KillOrphanThreads: no PID files found")
252
- return
253
- }
254
- log.Info("KillOrphanThreads: checking %d PID files for orphan processes", len(pids))
255
- killed := 0
256
- for threadID, pid := range pids {
257
- path := filepath.Join(pidsDir, threadID+".pid")
258
- if IsProcessAlive(pid) {
259
- log.Info("Killing orphan thread %s (PID %d)", threadID, pid)
260
- if err := KillProcess(pid, log); err != nil {
261
- log.Warn("Failed to kill orphan PID %d: %v", pid, err)
262
- }
263
- killed++
264
- }
265
- _ = os.Remove(path)
266
- }
267
- if killed > 0 {
268
- log.Info("KillOrphanThreads: killed %d orphan processes, cleaned %d PID files", killed, len(pids))
269
- } else {
270
- log.Info("KillOrphanThreads: no orphans found, cleaned %d stale PID files", len(pids))
271
- }
272
- }
273
-
274
- // CleanStalePIDs removes PID files for processes that are no longer running.
275
- func CleanStalePIDs(pidsDir string, log *Logger) {
276
- pids := ListThreadPIDs(pidsDir)
277
- if len(pids) == 0 {
278
- log.Debug("CleanStalePIDs: no PID files found in %s", pidsDir)
279
- return
280
- }
281
- log.Debug("CleanStalePIDs: checking %d PID files", len(pids))
282
- cleaned := 0
283
- for threadID, pid := range pids {
284
- if !IsProcessAlive(pid) {
285
- path := filepath.Join(pidsDir, threadID+".pid")
286
- log.Info("Removing stale PID file for thread %s (PID %d)", threadID, pid)
287
- _ = os.Remove(path)
288
- cleaned++
289
- }
290
- }
291
- if cleaned > 0 {
292
- log.Info("CleanStalePIDs: removed %d stale PID files", cleaned)
293
- }
294
- }
@@ -1,108 +0,0 @@
1
- package main
2
-
3
- import (
4
- "os"
5
- "path/filepath"
6
- "testing"
7
- )
8
-
9
- func TestReadPIDFile_JSON(t *testing.T) {
10
- dir := t.TempDir()
11
- path := filepath.Join(dir, "test.pid")
12
- os.WriteFile(path, []byte(`{"pid":12345}`), 0644)
13
-
14
- pid, err := ReadPIDFile(path)
15
- if err != nil {
16
- t.Fatalf("unexpected error: %v", err)
17
- }
18
- if pid != 12345 {
19
- t.Errorf("got %d, want 12345", pid)
20
- }
21
- }
22
-
23
- func TestReadPIDFile_RawInt(t *testing.T) {
24
- dir := t.TempDir()
25
- path := filepath.Join(dir, "test.pid")
26
- os.WriteFile(path, []byte("54321\n"), 0644)
27
-
28
- pid, err := ReadPIDFile(path)
29
- if err != nil {
30
- t.Fatalf("unexpected error: %v", err)
31
- }
32
- if pid != 54321 {
33
- t.Errorf("got %d, want 54321", pid)
34
- }
35
- }
36
-
37
- func TestReadPIDFile_Invalid(t *testing.T) {
38
- dir := t.TempDir()
39
- path := filepath.Join(dir, "test.pid")
40
- os.WriteFile(path, []byte("not-a-pid"), 0644)
41
-
42
- _, err := ReadPIDFile(path)
43
- if err == nil {
44
- t.Fatal("expected error for invalid PID content")
45
- }
46
- }
47
-
48
- func TestReadPIDFile_Missing(t *testing.T) {
49
- _, err := ReadPIDFile(filepath.Join(t.TempDir(), "missing.pid"))
50
- if err == nil {
51
- t.Fatal("expected error for missing file")
52
- }
53
- }
54
-
55
- func TestAtomicWrite(t *testing.T) {
56
- dir := t.TempDir()
57
- path := filepath.Join(dir, "data.txt")
58
-
59
- if err := atomicWrite(path, []byte("hello")); err != nil {
60
- t.Fatalf("atomicWrite failed: %v", err)
61
- }
62
- data, err := os.ReadFile(path)
63
- if err != nil {
64
- t.Fatalf("ReadFile failed: %v", err)
65
- }
66
- if string(data) != "hello" {
67
- t.Errorf("got %q, want %q", string(data), "hello")
68
- }
69
- }
70
-
71
- func TestListThreadPIDs(t *testing.T) {
72
- dir := t.TempDir()
73
- os.WriteFile(filepath.Join(dir, "1234.pid"), []byte(`{"pid":100}`), 0644)
74
- os.WriteFile(filepath.Join(dir, "5678.pid"), []byte("200"), 0644)
75
- os.WriteFile(filepath.Join(dir, "not-a-pid.txt"), []byte("300"), 0644)
76
-
77
- result := ListThreadPIDs(dir)
78
- if len(result) != 2 {
79
- t.Fatalf("got %d entries, want 2", len(result))
80
- }
81
- if result["1234"] != 100 {
82
- t.Errorf("result[1234] = %d, want 100", result["1234"])
83
- }
84
- if result["5678"] != 200 {
85
- t.Errorf("result[5678] = %d, want 200", result["5678"])
86
- }
87
- }
88
-
89
- func TestListThreadPIDs_MissingDir(t *testing.T) {
90
- result := ListThreadPIDs(filepath.Join(t.TempDir(), "no-such-dir"))
91
- if len(result) != 0 {
92
- t.Errorf("expected empty map for missing directory, got %d entries", len(result))
93
- }
94
- }
95
-
96
- func TestUpsertEnv_ReplacesExistingAndAppendsMissing(t *testing.T) {
97
- env := []string{"A=1", "B=2"}
98
-
99
- env = upsertEnv(env, "B", "updated")
100
- if env[1] != "B=updated" {
101
- t.Fatalf("expected existing key to be replaced, got %q", env[1])
102
- }
103
-
104
- env = upsertEnv(env, "C", "3")
105
- if env[len(env)-1] != "C=3" {
106
- t.Fatalf("expected missing key to be appended, got %q", env[len(env)-1])
107
- }
108
- }
@@ -1,14 +0,0 @@
1
- //go:build !windows
2
-
3
- package main
4
-
5
- import (
6
- "os/exec"
7
- "syscall"
8
- )
9
-
10
- func setSysProcAttr(cmd *exec.Cmd) {
11
- cmd.SysProcAttr = &syscall.SysProcAttr{
12
- Setsid: true, // detach from parent session
13
- }
14
- }
@@ -1,15 +0,0 @@
1
- //go:build windows
2
-
3
- package main
4
-
5
- import (
6
- "os/exec"
7
- "syscall"
8
- )
9
-
10
- func setSysProcAttr(cmd *exec.Cmd) {
11
- cmd.SysProcAttr = &syscall.SysProcAttr{
12
- CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
13
- HideWindow: true,
14
- }
15
- }
@@ -1,95 +0,0 @@
1
- package main
2
-
3
- import (
4
- "errors"
5
- "fmt"
6
- "os"
7
- "strconv"
8
-
9
- "github.com/zalando/go-keyring"
10
-
11
- sv "github.com/andriyshevchenko/SecureVault/securevault-go"
12
- )
13
-
14
- const defaultKeyringService = "sensorium-supervisor"
15
-
16
- var keyringGet = keyring.Get
17
-
18
- // resolveSecretWithKeyring returns environment value first, then keyring fallback.
19
- // If both sources are unavailable, it returns an empty string.
20
- func resolveSecretWithKeyring(envKey, keyringService string) string {
21
- if v := os.Getenv(envKey); v != "" {
22
- return v
23
- }
24
-
25
- if keyringService == "" {
26
- return ""
27
- }
28
-
29
- secret, err := keyringGet(keyringService, envKey)
30
- if err != nil {
31
- if errors.Is(err, keyring.ErrNotFound) {
32
- return ""
33
- }
34
- fmt.Fprintf(os.Stderr, "WARN: keyring lookup failed for %s (service=%s): %v\n", envKey, keyringService, err)
35
- return ""
36
- }
37
- return secret
38
- }
39
-
40
- // resolveIntWithKeyring parses an integer value from env first, then keyring fallback.
41
- // If parsing fails or no value exists, it returns fallback.
42
- func resolveIntWithKeyring(envKey, keyringService string, fallback int) int {
43
- v := resolveSecretWithKeyring(envKey, keyringService)
44
- if v == "" {
45
- return fallback
46
- }
47
- parsed, err := strconv.Atoi(v)
48
- if err != nil {
49
- fmt.Fprintf(os.Stderr, "WARN: invalid integer for %s: %q\n", envKey, v)
50
- return fallback
51
- }
52
- return parsed
53
- }
54
-
55
- // resolveFromSecureVault looks up envKey in the named SecureVault profile.
56
- // baseDir may be empty (defaults to %LOCALAPPDATA%\SecureVault).
57
- // Returns empty string on any error, with a warning for unexpected failures.
58
- func resolveFromSecureVault(profileName, envKey, baseDir string) string {
59
- store := sv.NewStore(baseDir)
60
- val, err := store.ResolveKey(profileName, envKey)
61
- if err != nil {
62
- if errors.Is(err, sv.ErrNotFound) || errors.Is(err, sv.ErrUnsupportedPlatform) {
63
- return ""
64
- }
65
- fmt.Fprintf(os.Stderr, "WARN: securevault lookup failed for %s (profile=%s): %v\n", envKey, profileName, err)
66
- return ""
67
- }
68
- return val
69
- }
70
-
71
- // resolveStringChain resolves a string config key using the chain:
72
- // environment variable → SecureVault profile → OS keyring → empty string.
73
- func resolveStringChain(envKey, svProfile, svBaseDir, keyringService string) string {
74
- if v := os.Getenv(envKey); v != "" {
75
- return v
76
- }
77
- if v := resolveFromSecureVault(svProfile, envKey, svBaseDir); v != "" {
78
- return v
79
- }
80
- return resolveSecretWithKeyring(envKey, keyringService)
81
- }
82
-
83
- // resolveIntChain resolves an integer config key using the same chain as resolveStringChain.
84
- func resolveIntChain(envKey, svProfile, svBaseDir, keyringService string, fallback int) int {
85
- v := resolveStringChain(envKey, svProfile, svBaseDir, keyringService)
86
- if v == "" {
87
- return fallback
88
- }
89
- parsed, err := strconv.Atoi(v)
90
- if err != nil {
91
- fmt.Fprintf(os.Stderr, "WARN: invalid integer for %s: %q\n", envKey, v)
92
- return fallback
93
- }
94
- return parsed
95
- }