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

> Discover how witr detects Cron and systemd timer schedules by examining process ancestry and integrating with systemd. Learn about witr's innovative detection methods.

- Repository: [Pranshu Parmar/witr](https://github.com/pranshuparmar/witr)
- Tags: internals
- Published: 2026-08-09

---

**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`](https://github.com/pranshuparmar/witr/blob/main/internal/source/cron.go)

The detection logic resides in the `detectCron` function within **[`internal/source/cron.go`](https://github.com/pranshuparmar/witr/blob/main/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.

```go
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`](https://github.com/pranshuparmar/witr/blob/main/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`](https://github.com/pranshuparmar/witr/blob/main/internal/proc/systemd_linux.go)

The core logic for systemd detection lives in **[`internal/proc/systemd_linux.go`](https://github.com/pranshuparmar/witr/blob/main/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.

```go
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`](https://github.com/pranshuparmar/witr/blob/main/pkg/model/source.go)**) and records the timer name and extracted schedule.

For non-Linux platforms, **[`internal/proc/systemd_stub.go`](https://github.com/pranshuparmar/witr/blob/main/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:

```go
// 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:

```go
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`](https://github.com/pranshuparmar/witr/blob/main/internal/source/cron.go)**.
- **systemd timer detection** uses `systemctl list-sockets` in **[`internal/proc/systemd_linux.go`](https://github.com/pranshuparmar/witr/blob/main/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`](https://github.com/pranshuparmar/witr/blob/main/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.