|
9 | 9 | "os/user" |
10 | 10 | "path/filepath" |
11 | 11 | "strconv" |
| 12 | + "strings" |
12 | 13 | "sync" |
13 | 14 | "syscall" |
14 | 15 | "time" |
@@ -89,6 +90,11 @@ type Service struct { |
89 | 90 |
|
90 | 91 | cred *syscall.Credential // resolved User=/Group=, applied to spawned procs |
91 | 92 |
|
| 93 | + // forkedPID is the daemon a Type=forking launcher fork()ed, learned from |
| 94 | + // PIDFile= after the launcher exits. Stop/kill target this, not the dead |
| 95 | + // launcher (R-SINIT6). |
| 96 | + forkedPID int |
| 97 | + |
92 | 98 | // Socket activation: if set, the main process is spawned via the sd-exec |
93 | 99 | // trampoline with these listening fds handed over as LISTEN_FDS. |
94 | 100 | Listeners []*socketact.Listener |
@@ -233,6 +239,14 @@ func (s *Service) startLongRunning(ctx context.Context) error { |
233 | 239 | s.setState(Failed) |
234 | 240 | return fmt.Errorf("%s: forking launcher failed", s.cfg.Name) |
235 | 241 | } |
| 242 | + // The launcher has exited; the real daemon is whatever it fork()ed and wrote |
| 243 | + // to PIDFile=. Learn that pid so Stop/kill hit the daemon, not the dead |
| 244 | + // launcher (R-SINIT6). No PIDFile -> we cannot track it (best effort). |
| 245 | + if pid := s.readPIDFile(ctx); pid > 0 { |
| 246 | + s.mu.Lock() |
| 247 | + s.forkedPID = pid |
| 248 | + s.mu.Unlock() |
| 249 | + } |
236 | 250 | s.setState(Active) |
237 | 251 | s.runPost(ctx) |
238 | 252 | return nil |
@@ -290,12 +304,53 @@ func (s *Service) startNotify(ctx context.Context) error { |
290 | 304 | func (s *Service) killMain() { |
291 | 305 | s.mu.Lock() |
292 | 306 | main := s.main |
| 307 | + forked := s.forkedPID |
293 | 308 | s.mu.Unlock() |
| 309 | + if forked > 0 { |
| 310 | + _ = syscall.Kill(forked, syscall.SIGKILL) // Type=forking: the daemon, not the launcher |
| 311 | + return |
| 312 | + } |
294 | 313 | if main != nil && main.Process != nil { |
295 | 314 | _ = main.Process.Kill() |
296 | 315 | } |
297 | 316 | } |
298 | 317 |
|
| 318 | +// readPIDFile reads PIDFile= for a Type=forking service. The daemon may write it a |
| 319 | +// moment after the launcher exits, so we retry briefly (bounded by ctx). |
| 320 | +func (s *Service) readPIDFile(ctx context.Context) int { |
| 321 | + if s.cfg.PIDFile == "" { |
| 322 | + return 0 |
| 323 | + } |
| 324 | + for i := 0; i < 20; i++ { |
| 325 | + if b, err := os.ReadFile(s.cfg.PIDFile); err == nil { |
| 326 | + if pid, err := strconv.Atoi(strings.TrimSpace(string(b))); err == nil && pid > 0 { |
| 327 | + return pid |
| 328 | + } |
| 329 | + } |
| 330 | + select { |
| 331 | + case <-ctx.Done(): |
| 332 | + return 0 |
| 333 | + case <-time.After(50 * time.Millisecond): |
| 334 | + } |
| 335 | + } |
| 336 | + return 0 |
| 337 | +} |
| 338 | + |
| 339 | +// pidAlive reports whether pid still exists (signal 0 probes without delivering). |
| 340 | +func pidAlive(pid int) bool { return pid > 0 && syscall.Kill(pid, 0) == nil } |
| 341 | + |
| 342 | +// waitPIDGone polls until pid is reaped or the timeout elapses; returns true if gone. |
| 343 | +func waitPIDGone(pid int, timeout time.Duration) bool { |
| 344 | + deadline := time.Now().Add(timeout) |
| 345 | + for time.Now().Before(deadline) { |
| 346 | + if !pidAlive(pid) { |
| 347 | + return true |
| 348 | + } |
| 349 | + time.Sleep(20 * time.Millisecond) |
| 350 | + } |
| 351 | + return !pidAlive(pid) |
| 352 | +} |
| 353 | + |
299 | 354 | // spawnMain starts ExecStart[0], records the process, and launches a waiter |
300 | 355 | // that captures the exit and closes the per-run done channel. For Type=notify |
301 | 356 | // it always passes NOTIFY_SOCKET, so a restarted instance can signal readiness. |
@@ -515,14 +570,24 @@ func (s *Service) Stop(ctx context.Context) error { |
515 | 570 | s.stopping = true |
516 | 571 | main := s.main |
517 | 572 | exited := s.exited |
| 573 | + forked := s.forkedPID |
518 | 574 | s.mu.Unlock() |
519 | 575 |
|
520 | 576 | s.setState(Deactivating) |
521 | 577 | for _, ec := range s.cfg.ExecStop { |
522 | 578 | _ = s.runToCompletion(ctx, ec) |
523 | 579 | } |
524 | 580 |
|
525 | | - if main != nil && main.Process != nil && exited != nil { |
| 581 | + if forked > 0 { |
| 582 | + // Type=forking: the launcher already exited; stop the daemon it fork()ed |
| 583 | + // (from PIDFile), not the dead launcher (R-SINIT6). There is no exit channel |
| 584 | + // for a process we did not spawn, so poll for it to be reaped after each signal. |
| 585 | + _ = syscall.Kill(forked, syscall.SIGTERM) |
| 586 | + if !waitPIDGone(forked, DefaultStopTimeout) { |
| 587 | + _ = syscall.Kill(forked, syscall.SIGKILL) |
| 588 | + waitPIDGone(forked, DefaultStopTimeout) |
| 589 | + } |
| 590 | + } else if main != nil && main.Process != nil && exited != nil { |
526 | 591 | _ = main.Process.Signal(syscall.SIGTERM) |
527 | 592 | select { |
528 | 593 | case <-exited: |
|
0 commit comments