How witr Detects Cron and systemd Timer Schedules: Process Ancestry and systemd Integration

witr detects Cron schedules by walking the process ancestry tree to find cron or crond parent processes, and identifies systemd timers by using systemctl to map listening ports to service units, then querying the associated .timer unit's OnCalendar property.

Understanding how workloads are scheduled is critical for modern observability. The open-source tool witr (pranshuparmar/witr) analyzes running processes to determine whether they were launched by traditional Cron daemons or modern systemd timers. This article examines the exact mechanisms witr uses to detect Cron and systemd timer schedules by walking through the production source code.

Cron Schedule Detection via Process Ancestry

witr treats a process’s ancestry—the chain of parent processes—as the primary signal that a workload is being launched by a cron daemon.

Ancestry Traversal in internal/source/cron.go

The detection logic resides in the detectCron function within internal/source/cron.go. This function accepts a slice of model.Process structs representing the process tree and iterates through each parent to identify the scheduler.

func detectCron(ancestry []model.Process) *model.Source {
    for _, p := range ancestry {
        base := filepath.Base(p.Command)
        if base == "cron" || base == "crond" {
            return &model.Source{
                Type: model.SourceCron,
                Name: "cron",
            }
        }
    }
    return nil
}

The function extracts the base name of each process executable using filepath.Base(p.Command). If it encounters either "cron" or "crond", it returns a model.Source struct with Type set to model.SourceCron (defined in pkg/model/source.go) and Name set to "cron".

Why Ancestry Detection Works

Cron jobs are always launched by the system’s cron daemon, typically located at /usr/sbin/cron or /usr/sbin/crond. By walking the process tree upward from the target process to the root, witr can definitively identify when the root ancestor is the cron daemon. This approach is reliable because the process relationship is preserved regardless of the specific Cron implementation (Vixie Cron, Cronie, or BusyBox cron), as long as the executable base name matches.

systemd Timer Detection via systemctl Integration

systemd timers are represented as .timer units that trigger corresponding .service units. witr discovers these relationships by interrogating systemd directly through the systemctl command-line interface.

Port-to-Service Resolution in internal/proc/systemd_linux.go

The core logic for systemd detection lives in internal/proc/systemd_linux.go (which includes a Linux-only build tag). The ResolveSystemdService function maps a listening network port to its owning systemd service unit.

func ResolveSystemdService(port int) (string, error) {
    if _, err := exec.LookPath("systemctl"); err != nil {
        return "", fmt.Errorf("systemctl not found")
    }

    cmd := exec.Command("systemctl", "list-sockets", "--no-legend", "--full")
    var out bytes.Buffer
    cmd.Stdout = &out
    if err := cmd.Run(); err != nil {
        return "", err
    }

    portStr := fmt.Sprintf(":%d", port)
    for _, line := range strings.Split(out.String(), "\n") {
        fields := strings.Fields(line)
        if len(fields) < 3 {
            continue
        }
        if strings.HasSuffix(fields[0], portStr) {
            return fields[2], nil // unit name, e.g. “myapp.service”
        }
    }
    return "", fmt.Errorf("no systemd service found for port %d", port)
}

This function first verifies that systemctl is available on the system using exec.LookPath. It then executes systemctl list-sockets --no-legend --full to obtain a mapping of listening sockets to unit names. By matching the requested port against the socket list, it extracts the associated unit name (typically a .service file).

Timer Unit Discovery and Schedule Extraction

Once witr identifies the service unit, it follows a four-step process to resolve the timer schedule:

  1. Identify the service unit – Via ResolveSystemdService as shown above.
  2. Map to the timer unit – systemd conventions name the timer identically to its service but with a .timer suffix instead of .service.
  3. Read the schedule – witr executes systemctl show <unit>.timer -p OnCalendar to retrieve the calendar expression (e.g., Mon *-*-* 03:00:00) that defines when the timer fires.
  4. Populate the source model – The system creates a model.Source with Type: model.SourceSystemdTimer (defined alongside SourceCron in pkg/model/source.go) and records the timer name and extracted schedule.

For non-Linux platforms, internal/proc/systemd_stub.go provides a no-op fallback implementation that returns an error, ensuring the codebase compiles across different operating systems.

Code Examples: Detecting Schedules with witr

Example 1: Identifying a Cron-Started Process

The following example demonstrates how to use witr’s source detection to identify if a process was launched by cron:

// Assume we have a slice of processes representing the ancestry of the target.
ancestry := []model.Process{
    {PID: 1234, Command: "/usr/sbin/cron"},
    {PID: 1235, Command: "/usr/bin/python3"},
}

// Detect the source.
src := source.detectCron(ancestry)
if src != nil && src.Type == model.SourceCron {
    fmt.Println("Process was launched by cron")
}

Output:


Process was launched by cron

Example 2: Resolving a systemd Timer for a Listening Port

This example shows how to resolve a systemd service from a network port and extract its associated timer schedule:

port := 8080
unit, err := proc.ResolveSystemdService(port)
if err != nil {
    log.Fatalf("cannot resolve systemd unit: %v", err)
}
fmt.Printf("Port %d is owned by systemd unit %s\n", port, unit)

// Mapping to timer (if a .timer exists)
timer := strings.TrimSuffix(unit, ".service") + ".timer"
out, _ := exec.Command("systemctl", "show", timer, "-p", "OnCalendar").
    Output()
fmt.Printf("Timer schedule: %s", out)

Typical output:


Port 8080 is owned by systemd unit myapp.service
Timer schedule: OnCalendar=Mon *-*-* 03:00:00

Summary

  • Cron detection relies on a simple ancestry walk through the process tree, looking for parent executables named cron or crond in internal/source/cron.go.
  • systemd timer detection uses systemctl list-sockets in internal/proc/systemd_linux.go to map ports to services, then follows naming conventions to locate .timer units and extract OnCalendar schedules.
  • Cross-platform support is handled via build tags, with a stub implementation for non-Linux systems.
  • Source models are standardized in pkg/model/source.go, using constants SourceCron and SourceSystemdTimer to represent the detected schedule types.

Frequently Asked Questions

How does witr distinguish between different Cron implementations?

witr checks only the base executable name of the parent process for "cron" or "crond" in the detectCron function. This covers the majority of Linux Cron implementations (including Vixie Cron, Cronie, and the BusyBox cron daemon) since they typically install binaries with these standard names. Alternative implementations using different executable names would not be detected by the current logic.

What happens if systemctl is not available on the system?

The ResolveSystemdService function explicitly checks for the presence of systemctl using exec.LookPath at the start of execution. If the binary is not found, the function immediately returns an error indicating "systemctl not found", and witr will not attempt to classify the process as a systemd timer workload.

Can witr detect user-level systemd timers?

Yes, because the systemctl list-sockets command used by ResolveSystemdService includes both system-level and user-level sockets in its output when run with appropriate permissions. However, witr maps the service to a timer by convention (replacing .service with .timer), so it will successfully identify user timers as long as they follow the standard systemd naming convention and the user has permission to query the systemd state.

Why does witr check process ancestry instead of environment variables for Cron detection?

Process ancestry provides a more reliable signal than environment variables because cron daemons do not always inject identifying environment variables into child processes, and variables like CRON or LOGNAME can be spoofed or missing depending on the Cron configuration. The process parent relationship is enforced by the kernel and cannot be forged by the child process, making the ancestry walk in detectCron a trustworthy detection mechanism.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →