wavetermdev/waveterm · error

failed to setsid: %w

Error message

failed to setsid: %w

What it means

daemonize in jobmanager_unix.go calls unix.Setsid() to detach the job process into a new session, a standard daemonization step. This error wraps the raw errno from Setsid. Setsid only fails when the calling process is already a process group leader (EPERM), which happens when daemonize is invoked from the wrong process or too late in fork/exec ordering.

Source

Thrown at pkg/jobmanager/jobmanager_unix.go:23

package jobmanager

import (
	"fmt"
	"log"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"

	"github.com/wavetermdev/waveterm/pkg/wavebase"
	"golang.org/x/sys/unix"
)

func daemonize(clientId string, jobId string) error {
	_, err := unix.Setsid()
	if err != nil {
		return fmt.Errorf("failed to setsid: %w", err)
	}

	devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
	if err != nil {
		return fmt.Errorf("failed to open /dev/null: %w", err)
	}
	err = unix.Dup2(int(devNull.Fd()), int(os.Stdin.Fd()))
	if err != nil {
		return fmt.Errorf("failed to dup2 stdin: %w", err)
	}
	devNull.Close()

	logPath := wavebase.GetRemoteJobFilePath(jobId, "log")
	logDir := filepath.Dir(logPath)
	err = os.MkdirAll(logDir, 0700)
	if err != nil {
		return fmt.Errorf("failed to create log directory: %w", err)
	}

View on GitHub (pinned to a4447c1563)

Solutions

  1. Ensure daemonize runs in a freshly forked child (e.g. via syscall.ForkExec/Reexec) so it is not a group leader
  2. Remove any earlier setpgid/Setpgid call on the process before daemonize
  3. Treat EPERM as 'already detached' if double-daemonization is possible, and continue

Example fix

// before
if _, err := unix.Setsid(); err != nil {
    return fmt.Errorf("failed to setsid: %w", err)
}
// after
if _, err := unix.Setsid(); err != nil {
    if errors.Is(err, unix.EPERM) {
        // already a session leader; treat as detached
        return nil
    }
    return fmt.Errorf("failed to setsid: %w", err)
}
Defensive patterns

Strategy: fallback

Validate before calling

// in the child process, before daemonize:
// guarantee this process is NOT a process-group leader
// (fork/exec or re-exec a fresh child that runs daemonize as its first action)

Try / catch

if err := daemonize(clientId, jobId); err != nil {
    if strings.Contains(err.Error(), "failed to setsid") {
        // EPERM: already a session/group leader — either skip or re-exec a child
    }
    return err
}

Prevention

When it happens

Trigger: unix.Setsid() returns EPERM because the calling process is already a process-group leader — e.g. daemonize invoked directly from the shell-facing parent process instead of a freshly forked child, or after setpgid was already applied.

Common situations: Calling the daemonize path from an interactive job process that the terminal already made a group leader; job control (SIGTTOU/setpgid) interference; embedding the job manager in a process that already called Setsid.

Related errors


AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01). Data as JSON: /api/errors/f60097c617bd30df. Report an issue: GitHub.