wavetermdev/waveterm · error

failed to open log file: %w

Error message

failed to open log file: %w

What it means

daemonize opens the job log file (O_CREATE|O_WRONLY|O_APPEND, 0600) and this error wraps the os.OpenFile failure. The daemon's stdout/stderr will be redirected to this file, so the job cannot start logging until it opens successfully.

Source

Thrown at pkg/jobmanager/jobmanager_unix.go:45

	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)
	}

	logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
	if err != nil {
		return fmt.Errorf("failed to open log file: %w", err)
	}
	err = unix.Dup2(int(logFile.Fd()), int(os.Stdout.Fd()))
	if err != nil {
		return fmt.Errorf("failed to dup2 stdout: %w", err)
	}
	err = unix.Dup2(int(logFile.Fd()), int(os.Stderr.Fd()))
	if err != nil {
		return fmt.Errorf("failed to dup2 stderr: %w", err)
	}

	log.SetOutput(logFile)
	log.Printf("job manager daemonized, logging to %s\n", logPath)
	log.Printf("job owner clientid: %s\n", clientId)

	signal.Ignore(syscall.SIGHUP)

	return nil
}

View on GitHub (pinned to a4447c1563)

Solutions

  1. Verify the parent directory exists and is writable by the current uid (it is just created by MkdirAll 0700)
  2. Remove or chown a stale log file owned by another uid: ls -l <logPath>
  3. Free disk space / check quota if errno is ENOSPC
  4. Shorten the jobId/path if NAME_MAX (255 bytes per component) is exceeded

Example fix

// before
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
    return fmt.Errorf("failed to open log file: %w", err)
}
// after
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
    if errors.Is(err, syscall.EACCES) {
        os.Remove(logPath) // drop stale file owned by another uid
        logFile, err = os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
    }
    if err != nil {
        return fmt.Errorf("failed to open log file %s: %w", logPath, err)
    }
}
Defensive patterns

Strategy: retry

Validate before calling

logPath := wavebase.GetRemoteJobFilePath(jobId, "log")
if len(filepath.Base(logPath)) > 255 {
    return fmt.Errorf("log file name exceeds NAME_MAX")
}
if fi, err := os.Stat(filepath.Dir(logPath)); err != nil || !fi.IsDir() {
    return fmt.Errorf("log directory missing/unwritable")
}

Try / catch

if err := daemonize(clientId, jobId); err != nil {
    if strings.Contains(err.Error(), "failed to open log file") {
        log.Printf("log open error: %v", errors.Unwrap(err))
        // EACCES: remove/chown stale file; ENOSPC: free space; then retry daemonize
    }
    return err
}

Prevention

When it happens

Trigger: os.OpenFile(logPath, O_CREATE|O_WRONLY|O_APPEND, 0600) fails: log directory not writable, ENOSPC, path exceeds NAME_MAX, or the path exists with ownership/permissions preventing the current uid from writing.

Common situations: Log directory owned by a different uid after a prior run under root/sudo; quota or disk-full on the data volume; overly long jobId inflating the file path; immutable or ACL-restricted file left from a previous session.

Related errors


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