Files
hyprvoice/internal/bus/bus.go
T
burakgizliceandClaude Opus 4.6 13217f52f8 fix: detect stale PID files from recycled PIDs
The daemon startup check only used kill -0 to verify if the PID from
the PID file was alive. If the OS recycled that PID for an unrelated
process, hyprvoice would refuse to start with "daemon already running"
even though no hyprvoice instance was running.

Now also reads /proc/<pid>/cmdline to verify the process is actually
hyprvoice before treating it as a running daemon. If it's a different
process, the PID file is treated as stale and removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 09:50:55 +03:00

239 lines
5.1 KiB
Go

package bus
import (
"bufio"
"fmt"
"log"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
)
const (
SockName = "control.sock"
PidName = "hyprvoice.pid"
ProtoVer = "0.1"
)
type pidManager struct {
path string
}
func newPidManager() (*pidManager, error) {
pidPath, err := getPidPath()
if err != nil {
return nil, fmt.Errorf("failed to get PID path: %w", err)
}
return &pidManager{path: pidPath}, nil
}
func (pm *pidManager) checkExisting() error {
log.Printf("Checking for existing daemon at: %s", pm.path)
pidData, err := os.ReadFile(pm.path)
if os.IsNotExist(err) {
log.Printf("No PID file found, daemon not running")
return nil
}
if err != nil {
return fmt.Errorf("error reading PID file: %w", err)
}
log.Printf("Found PID file with content: %s", string(pidData))
pid, err := strconv.Atoi(string(pidData))
if err != nil {
log.Printf("Invalid PID in file, removing stale PID file: %v", err)
pm.removeStaleFile()
return nil
}
if pm.isProcessAlive(pid) {
log.Printf("Process %d is alive, daemon already running", pid)
return fmt.Errorf("daemon already running with PID %d", pid)
}
log.Printf("Process %d not alive, removing stale PID file", pid)
pm.removeStaleFile()
return nil
}
func (pm *pidManager) create() error {
if err := os.MkdirAll(filepath.Dir(pm.path), 0o700); err != nil {
return fmt.Errorf("failed to create PID directory: %w", err)
}
pid := os.Getpid()
log.Printf("Creating PID file at %s with PID %d", pm.path, pid)
err := os.WriteFile(pm.path, []byte(strconv.Itoa(pid)), 0o600)
if err != nil {
return fmt.Errorf("failed to write PID file: %w", err)
}
return nil
}
func (pm *pidManager) remove() error {
log.Printf("Removing PID file: %s", pm.path)
if err := os.Remove(pm.path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove PID file: %w", err)
}
return nil
}
func (pm *pidManager) isProcessAlive(pid int) bool {
log.Printf("Checking if process %d is alive", pid)
proc, err := os.FindProcess(pid)
if err != nil {
log.Printf("Process %d not found: %v", pid, err)
return false
}
err = proc.Signal(syscall.Signal(0))
if err != nil {
log.Printf("Process %d not alive (signal failed: %v)", pid, err)
return false
}
// Verify the process is actually hyprvoice and not a recycled PID
cmdline, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
if err != nil {
log.Printf("Process %d alive but cannot read cmdline, assuming stale: %v", pid, err)
return false
}
exe := string(cmdline)
if len(exe) == 0 || !strings.Contains(exe, "hyprvoice") {
log.Printf("Process %d is alive but is not hyprvoice (cmdline: %q), stale PID file", pid, exe)
return false
}
return true
}
func (pm *pidManager) removeStaleFile() {
if err := os.Remove(pm.path); err != nil && !os.IsNotExist(err) {
log.Printf("Warning: failed to remove stale PID file: %v", err)
}
}
type socketManager struct {
path string
}
func newSocketManager() (*socketManager, error) {
sockPath, err := getSockPath()
if err != nil {
return nil, fmt.Errorf("failed to get socket path: %w", err)
}
return &socketManager{path: sockPath}, nil
}
func (sm *socketManager) listen() (net.Listener, error) {
if err := os.MkdirAll(filepath.Dir(sm.path), 0o700); err != nil {
return nil, fmt.Errorf("failed to create socket directory: %w", err)
}
os.Remove(sm.path)
listener, err := net.Listen("unix", sm.path)
if err != nil {
return nil, fmt.Errorf("failed to listen on socket %s: %w", sm.path, err)
}
return listener, nil
}
func (sm *socketManager) dial() (net.Conn, error) {
conn, err := net.Dial("unix", sm.path)
if err != nil {
return nil, fmt.Errorf("failed to dial socket %s: %w", sm.path, err)
}
return conn, nil
}
func getSockPath() (string, error) {
dir, err := os.UserCacheDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "hyprvoice", SockName), nil
}
func getPidPath() (string, error) {
dir, err := os.UserCacheDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "hyprvoice", PidName), nil
}
func SockPath() (string, error) {
return getSockPath()
}
func Listen() (net.Listener, error) {
sm, err := newSocketManager()
if err != nil {
return nil, err
}
return sm.listen()
}
func Dial() (net.Conn, error) {
sm, err := newSocketManager()
if err != nil {
return nil, err
}
return sm.dial()
}
func CheckExistingDaemon() error {
pm, err := newPidManager()
if err != nil {
return err
}
return pm.checkExisting()
}
func CreatePidFile() error {
pm, err := newPidManager()
if err != nil {
return err
}
return pm.create()
}
func RemovePidFile() error {
pm, err := newPidManager()
if err != nil {
return err
}
return pm.remove()
}
func SendCommand(cmd byte) (string, error) {
c, err := Dial()
if err != nil {
return "", fmt.Errorf("failed to connect to daemon: %w", err)
}
defer c.Close()
_, err = c.Write([]byte{cmd, '\n'})
if err != nil {
return "", fmt.Errorf("failed to send command: %w", err)
}
resp, err := bufio.NewReader(c).ReadString('\n')
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
return resp, nil
}