netatalk.io

Dev Docs Netatalk Controller Daemon

Netatalk Controller Daemon

1. Overview

The netatalk daemon is the master service controller for Netatalk’s AFP file-sharing stack. It orchestrates the lifecycle of child processes — afpd (AFP protocol daemon), cnid_metad (CNID metadata daemon), and optionally a D-Bus session daemon for Spotlight — through a libevent2-based event loop. The controller handles configuration parsing, service startup/restart, signal-driven reload and shutdown, and Zeroconf service registration.

Key Source Files

Component File
Master daemon etc/netatalk/netatalk.c
Zeroconf facade etc/netatalk/afp_zeroconf.c, etc/netatalk/afp_zeroconf.h
Avahi integration etc/netatalk/afp_avahi.c, etc/netatalk/afp_avahi.h
mDNSResponder integration etc/netatalk/afp_mdns.c, etc/netatalk/afp_mdns.h
Build definition etc/netatalk/meson.build
AFPObj / afp_options include/atalk/globals.h
Config parsing API include/atalk/netatalk_conf.h
Config parsing impl libatalk/util/netatalk_conf.c

Process Hierarchy

The netatalk daemon manages exactly three child processes. AppleTalk-era daemons (atalkd, papd, timelord, a2boot, macipgw) are not managed by netatalk — they have their own independent init scripts.

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph TB
    subgraph "Init System"
        INIT["systemd / launchd<br/>rc.d / OpenRC / SMF"]:::grey
    end

    subgraph "netatalk Master Daemon"
        NM["netatalk<br/>libevent2 event loop<br/>netatalk.c: main()"]:::orange
    end

    subgraph "Managed Child Processes"
        AFPD["afpd<br/>AFP file server<br/>etc/afpd/main.c"]:::blue
        CNID["cnid_metad<br/>CNID metadata daemon<br/>etc/cnid_dbd/cnid_metad.c"]:::purple
        DBUS["dbus-daemon<br/>D-Bus session bus<br/>Spotlight only"]:::grey
    end

    subgraph "Zeroconf Registration"
        ZC["afp_zeroconf.c<br/>Avahi or mDNSResponder"]:::green
    end

    subgraph "Spawned by cnid_metad"
        CNIDDBD["cnid_dbd<br/>per-volume CNID<br/>database daemon"]:::salmon
    end

    INIT -->|"start/stop"| NM
    NM -->|"fork+exec"| AFPD
    NM -->|"fork+exec<br/>if CNID_BACKEND_DBD"| CNID
    NM -->|"fork+exec<br/>if WITH_SPOTLIGHT"| DBUS
    NM --> ZC
    CNID -->|"spawns per volume"| CNIDDBD

    classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
    classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
    classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
    classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
    classDef grey fill:#dfe6e9,stroke:#333,rx:10,ry:10
    classDef salmon fill:#fab1a0,stroke:#333,rx:10,ry:10
    classDef cyan fill:#81ecec,stroke:#333,rx:10,ry:10
    classDef orange fill:#ff9f43,stroke:#333,rx:10,ry:10

2. Service State Model

Three sentinel constants defined in netatalk.c are used as PID values to track service state:

#define NETATALK_SRV_NEEDED  -1   // Service must be (re)started
#define NETATALK_SRV_OPTIONAL 0   // Service not required
#define NETATALK_SRV_ERROR    NETATALK_SRV_NEEDED  // Error → needs restart

Global State

Defined as file-scope variables in netatalk.c:

Variable Storage Type Purpose
obj static AFPObj Parsed configuration object
afpd_pid static pid_t AFP daemon PID or sentinel
cnid_metad_pid static pid_t CNID metadata daemon PID or sentinel
dbus_pid static pid_t D-Bus session daemon PID or sentinel
afpd_restarts static uint AFP daemon restart counter
cnid_metad_restarts static uint CNID daemon restart counter
dbus_restarts static uint D-Bus daemon restart counter
base static struct event_base * libevent2 event base
sigterm_ev, sigquit_ev, sigchld_ev, sighup_ev, timer_ev global struct event * libevent2 signal/timer events
in_shutdown static int Shutdown state flag
dbus_path static const char * Configured dbus-daemon path

Initial PID values control which services are managed:

The service_running() helper checks whether a PID represents an active process:

static bool service_running(pid_t pid)
{
    if ((pid != NETATALK_SRV_NEEDED) && (pid != NETATALK_SRV_OPTIONAL))
        return true;
    return false;
}

3. Startup Sequence

The main() function in netatalk.c implements a carefully ordered startup:

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
sequenceDiagram
    participant Init as Init System
    participant NM as netatalk main()
    participant AFP as afpd
    participant CNID as cnid_metad
    participant DBUS as dbus-daemon
    participant ZC as Zeroconf

    Init->>NM: Start netatalk
    NM->>NM: Parse -d / -F flags
    NM->>NM: check_lockfile()
    NM->>NM: daemonize() unless -d
    NM->>NM: create_lockfile()
    NM->>NM: Block all signals
    NM->>NM: afp_config_parse(&obj, "netatalk")
    NM->>NM: load_volumes(&obj, LV_ALL)

    NM->>AFP: run_process(_PATH_AFPD, "-d", "-F", configfile)
    AFP-->>NM: afpd_pid

    alt CNID_BACKEND_DBD compiled in
        NM->>CNID: run_process(_PATH_CNID_METAD, "-d", "-F", configfile)
        CNID-->>NM: cnid_metad_pid
    end

    NM->>NM: Setup libevent2 base + signal events
    NM->>NM: Start 1-second periodic timer_ev

    alt WITH_SPOTLIGHT && OPTION_SPOTLIGHT
        NM->>NM: Set DBUS_SESSION_BUS_ADDRESS env
        NM->>DBUS: run_process(dbus_path, "--config-file=...")
        DBUS-->>NM: dbus_pid
        NM->>NM: sleep(1) — let D-Bus start
        NM->>NM: set_sl_volumes() via gsettings
        NM->>NM: system(INDEXER_COMMAND " -s")
    end

    NM->>ZC: zeroconf_register(&obj)
    NM->>NM: event_base_dispatch(base)
    Note over NM: Enter libevent event loop

Command-Line Interface

usage: netatalk [-F configfile]
       netatalk -d              # debug mode (foreground)
       netatalk -v|-V           # show version and paths

The show_netatalk_version() function displays compiled-in feature support (Zeroconf, Spotlight). The show_netatalk_paths() function displays key file paths:

Path Macro Build Source
afp.conf _PATH_CONFDIR "afp.conf" pkgconfdir in meson.build
afpd _PATH_AFPD sbindir/afpd
cnid_metad _PATH_CNID_METAD sbindir/cnid_metad
dbus-daemon DBUS_DAEMON_PATH Detected at build time
dbus-session.conf _PATH_CONFDIR "dbus-session.conf" pkgconfdir
indexer command INDEXER_COMMAND LocalSearch3 or Tracker3
lock file PATH_NETATALK_LOCK lockfile_path/netatalk

4. Process Management

Fork and Exec

The run_process() function uses fork() + execv() to launch child processes. It accepts a variable argument list of command-line parameters:

static pid_t run_process(const char *path, ...)
{
    int i = 0;
#define MYARVSIZE 64
    char *myargv[MYARVSIZE];
    va_list args;
    pid_t pid;

    if ((pid = fork()) < 0) {
        LOG(log_error, logtype_cnid, "error in fork: %s", strerror(errno));
        return -1;
    }

    if (pid == 0) {
        myargv[i++] = (char *)path;
        va_start(args, path);
        while (i < MYARVSIZE) {
            if ((myargv[i++] = va_arg(args, char *)) == NULL)
                break;
        }
        va_end(args);
        (void)execv(path, myargv);
        LOG(log_error, logtype_cnid, "Fatal error in exec: %s", strerror(errno));
        exit(1);
    }

    return pid;
}

All child processes are launched with -d (debug/foreground mode) and -F configfile so they inherit the same configuration file path and run as foreground daemons under netatalk’s supervision.

Kill Children

The kill_childs() function sends a signal to a variable-length list of child PID pointers, skipping any that hold sentinel values:

static void kill_childs(int sig, ...)
{
    va_list args;
    pid_t *pid;
    va_start(args, sig);

    while ((pid = va_arg(args, pid_t *)) != NULL) {
        if (*pid == NETATALK_SRV_ERROR || *pid == NETATALK_SRV_OPTIONAL)
            continue;
        kill(*pid, sig);
    }

    va_end(args);
}

Automatic Restart via Timer

The timer_cb() callback fires every 1 second and restarts any service whose PID holds the NETATALK_SRV_NEEDED sentinel:

static void timer_cb(evutil_socket_t fd, short what, void *arg)
{
    if (in_shutdown)
        return;

    if (afpd_pid == NETATALK_SRV_NEEDED) {
        afpd_restarts++;
        LOG(log_note, logtype_afpd, "Restarting 'afpd' (restarts: %u)", afpd_restarts);
        if ((afpd_pid = run_process(_PATH_AFPD, "-d", "-F", obj.options.configfile,
                                    NULL)) == -1) {
            LOG(log_error, logtype_default, "Error starting 'afpd'");
        }
    }

    if (cnid_metad_pid == NETATALK_SRV_NEEDED) {
        cnid_metad_restarts++;
        LOG(log_note, logtype_afpd, "Restarting 'cnid_metad' (restarts: %u)",
            cnid_metad_restarts);
        if ((cnid_metad_pid = run_process(_PATH_CNID_METAD, "-d", "-F",
                                          obj.options.configfile, NULL)) == -1) {
            LOG(log_error, logtype_default, "Error starting 'cnid_metad'");
        }
    }

#ifdef WITH_SPOTLIGHT
    if (dbus_pid == NETATALK_SRV_NEEDED) {
        dbus_restarts++;
        LOG(log_note, logtype_afpd, "Restarting 'dbus' (restarts: %u)", dbus_restarts);
        if ((dbus_pid = run_process(dbus_path,
                                    "--config-file=" _PATH_CONFDIR "dbus-session.conf",
                                    NULL)) == -1) {
            LOG(log_error, logtype_default, "Error starting '%s'", dbus_path);
        }
    }
#endif
}

There is no restart limit — services are restarted indefinitely as long as in_shutdown is not set. The restart counter is logged but not used to cap retries.


5. Signal Handling

The netatalk daemon uses libevent2 signal events for all signal handling, set up in main():

sigterm_ev = event_new(base, SIGTERM, EV_SIGNAL, sigterm_cb, NULL);
sigquit_ev = event_new(base, SIGQUIT, EV_SIGNAL | EV_PERSIST, sigquit_cb, NULL);
sighup_ev  = event_new(base, SIGHUP,  EV_SIGNAL | EV_PERSIST, sighup_cb, NULL);
sigchld_ev = event_new(base, SIGCHLD, EV_SIGNAL | EV_PERSIST, sigchld_cb, NULL);
timer_ev   = event_new(base, -1, EV_PERSIST, timer_cb, NULL);

Only SIGTERM, SIGQUIT, SIGCHLD, and SIGHUP are unblocked; all other signals are blocked via sigprocmask().

Signal Flow

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph LR
    subgraph "Signals"
        SIGHUP["SIGHUP<br/>Reload"]:::green
        SIGTERM["SIGTERM<br/>Graceful stop"]:::salmon
        SIGQUIT["SIGQUIT<br/>Immediate stop"]:::orange
        SIGCHLD["SIGCHLD<br/>Child exited"]:::blue
    end

    subgraph "Callbacks"
        HUP["sighup_cb()"]:::green
        TERM["sigterm_cb()"]:::salmon
        QUIT["sigquit_cb()"]:::orange
        CHLD["sigchld_cb()"]:::blue
    end

    subgraph "Actions"
        RELOAD["load_volumes()<br/>Re-register Zeroconf<br/>Forward SIGHUP to<br/>afpd + cnid_metad"]:::green
        GRACE["in_shutdown = 1<br/>Block signals (except SIGCHLD)<br/>event_base_loopexit(5s)<br/>Stop Spotlight indexer<br/>SIGTERM to children"]:::salmon
        IMMEDIATE["Stop Spotlight indexer<br/>SIGQUIT to all children"]:::orange
        REAP["waitpid() loop<br/>Set PID to SRV_ERROR<br/>If all exited during shutdown:<br/>event_base_loopbreak()"]:::blue
    end

    SIGHUP --> HUP --> RELOAD
    SIGTERM --> TERM --> GRACE
    SIGQUIT --> QUIT --> IMMEDIATE
    SIGCHLD --> CHLD --> REAP

    classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
    classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
    classDef salmon fill:#fab1a0,stroke:#333,rx:10,ry:10
    classDef orange fill:#ff9f43,stroke:#333,rx:10,ry:10

SIGHUP — Configuration Reload

The sighup_cb() handler reloads volumes, re-registers with Zeroconf, and forwards SIGHUP to afpd and cnid_metad:

static void sighup_cb(evutil_socket_t fd, short what, void *arg)
{
    LOG(log_note, logtype_afpd,
        "Received SIGHUP, sending all processes signal to reload config");
    load_volumes(&obj, LV_ALL);

    if (!(obj.options.flags & OPTION_NOZEROCONF)) {
        zeroconf_deregister();
        zeroconf_register(&obj);
        LOG(log_note, logtype_default, "Re-registered with Zeroconf");
    }

    kill_childs(SIGHUP, &afpd_pid, &cnid_metad_pid, NULL);
}

When afpd receives SIGHUP, its own afp_goaway() handler in etc/afpd/main.c sets reloadconfig = 1, which triggers a full re-parse of afp.conf in the parent’s main poll loop.

SIGTERM — Graceful Shutdown

The sigterm_cb() handler initiates a timed graceful shutdown with a KILL_GRACETIME of 5 seconds:

static void sigterm_cb(evutil_socket_t fd, short what, void *arg)
{
    sigset_t sigs;
    struct timeval tv;
    LOG(log_info, logtype_afpd, "Exiting on SIGTERM");

    if (in_shutdown)
        return;

    in_shutdown = 1;
    sigfillset(&sigs);
    sigdelset(&sigs, SIGCHLD);
    sigprocmask(SIG_SETMASK, &sigs, NULL);

    tv.tv_sec = KILL_GRACETIME;
    tv.tv_usec = 0;
    event_base_loopexit(base, &tv);
    event_del(sigterm_ev);
    event_del(sigquit_ev);
    event_del(sighup_ev);
    event_del(timer_ev);

#ifdef WITH_SPOTLIGHT
    system(INDEXER_COMMAND " -t");
#endif

    kill_childs(SIGTERM, &afpd_pid, &cnid_metad_pid, &dbus_pid, NULL);
}

After the event loop exits (either all children exited or timeout), main() checks for stragglers and sends SIGKILL:

if (service_running(afpd_pid) || service_running(cnid_metad_pid)
        || service_running(dbus_pid)) {
    // Log which services didn't shut down...
    kill_childs(SIGKILL, &afpd_pid, &cnid_metad_pid, &dbus_pid, NULL);
}

SIGQUIT — Immediate Shutdown

The sigquit_cb() handler sends SIGQUIT directly to all children without the graceful shutdown sequence:

static void sigquit_cb(evutil_socket_t fd, short what, void *arg)
{
    LOG(log_note, logtype_afpd, "Exiting on SIGQUIT");
#ifdef WITH_SPOTLIGHT
    system(INDEXER_COMMAND " -t");
#endif
    kill_childs(SIGQUIT, &afpd_pid, &cnid_metad_pid, &dbus_pid, NULL);
}

SIGCHLD — Child Process Reaping

The sigchld_cb() handler uses a waitpid() loop to reap all terminated children, identifies which service exited, and sets its PID to NETATALK_SRV_ERROR (which equals NETATALK_SRV_NEEDED, triggering restart by timer_cb()):

static void sigchld_cb(evutil_socket_t fd, short what, void *arg)
{
    int status;
    pid_t pid;

    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            if (WEXITSTATUS(status))
                LOG(log_info, logtype_default, "child[%d]: exited %d",
                    pid, WEXITSTATUS(status));
            else
                LOG(log_info, logtype_default, "child[%d]: done", pid);
        } else {
            if (WIFSIGNALED(status))
                LOG(log_info, logtype_default, "child[%d]: killed by signal %d",
                    pid, WTERMSIG(status));
            else
                LOG(log_info, logtype_default, "child[%d]: died", pid);
        }

        if (pid == afpd_pid)
            afpd_pid = NETATALK_SRV_ERROR;
        else if (pid == cnid_metad_pid)
            cnid_metad_pid = NETATALK_SRV_ERROR;
        else if (pid == dbus_pid)
            dbus_pid = NETATALK_SRV_ERROR;
        else
            LOG(log_error, logtype_afpd, "Bad pid: %d", pid);
    }

    if (in_shutdown
            && !service_running(afpd_pid)
            && !service_running(cnid_metad_pid)
            && !service_running(dbus_pid)) {
        event_base_loopbreak(base);
    }
}

During shutdown, once all three services have exited, event_base_loopbreak() immediately terminates the event loop, bypassing the KILL_GRACETIME timeout.


6. Shutdown Sequence

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
sequenceDiagram
    participant SYS as System / Init
    participant NM as netatalk
    participant AFP as afpd
    participant CNID as cnid_metad
    participant DBUS as dbus-daemon

    SYS->>NM: SIGTERM
    NM->>NM: in_shutdown = 1
    NM->>NM: Block all signals except SIGCHLD
    NM->>NM: event_base_loopexit(KILL_GRACETIME=5s)
    NM->>NM: Stop Spotlight indexer (if enabled)
    NM->>AFP: SIGTERM
    NM->>CNID: SIGTERM
    NM->>DBUS: SIGTERM

    alt All children exit within 5s
        AFP-->>NM: SIGCHLD (exit)
        CNID-->>NM: SIGCHLD (exit)
        DBUS-->>NM: SIGCHLD (exit)
        NM->>NM: event_base_loopbreak()
    else Timeout after 5s
        NM->>NM: Event loop exits
        NM->>AFP: SIGKILL (if still running)
        NM->>CNID: SIGKILL (if still running)
        NM->>DBUS: SIGKILL (if still running)
    end

    NM->>NM: server_unlock(PATH_NETATALK_LOCK)
    NM->>NM: exit()

7. Configuration Loading

Parse and Distribute

At startup, main() calls afp_config_parse() (declared in include/atalk/netatalk_conf.h, implemented in libatalk/util/netatalk_conf.c) with "netatalk" as the process name, then load_volumes() with LV_ALL to load all volume definitions:

if (afp_config_parse(&obj, "netatalk") != 0)
    netatalk_exit(EXITERR_CONF);

load_volumes(&obj, LV_ALL);

The global AFPObj obj (defined in include/atalk/globals.h) holds parsed configuration. Key fields used by the controller:

Field Type Purpose
obj.options.configfile char * Path to afp.conf
obj.options.flags int Bitmask: OPTION_NOZEROCONF, OPTION_SPOTLIGHT, etc.
obj.iniconfig dictionary * Parsed INI file for INIPARSER_GETSTR() lookups

The configuration file path is passed to child processes via the -F flag, so each child parses afp.conf independently with afp_config_parse() using its own process name.

Spotlight Configuration

When WITH_SPOTLIGHT is compiled in and OPTION_SPOTLIGHT is set in obj.options.flags, main() configures the D-Bus session environment and Tracker/LocalSearch indexer:

if (obj.options.flags & OPTION_SPOTLIGHT) {
    setenv("DBUS_SESSION_BUS_ADDRESS", "unix:path=" _PATH_STATEDIR "spotlight.ipc", 1);
    setenv("XDG_DATA_HOME", _PATH_STATEDIR, 0);
    setenv("XDG_CACHE_HOME", _PATH_STATEDIR, 0);
    setenv("TRACKER_USE_LOG_FILES", "1", 0);

    dbus_path = INIPARSER_GETSTR(obj.iniconfig, INISEC_GLOBAL, "dbus daemon",
                                 DBUS_DAEMON_PATH);
    // ... launch dbus, sleep(1), set_sl_volumes(), start indexer
}

The set_sl_volumes() function iterates all loaded volumes. For each volume with the AFPVOL_SPOTLIGHT flag, it builds a comma-separated path list and configures the indexer via gsettings:

cmd = bformat("gsettings set " INDEXER_DBUS_NAME
              " index-recursive-directories \"[%s]\"",
              bdata(volnamelist) ? bdata(volnamelist) : "");

The INDEXER_DBUS_NAME and INDEXER_COMMAND macros are set in meson.build depending on whether LocalSearch3 or Tracker3 is available.


8. Zeroconf Service Registration

Architecture

The Zeroconf subsystem uses a facade pattern. afp_zeroconf.c delegates to either afp_avahi.c or afp_mdns.c:

void zeroconf_register(const AFPObj *configs)
{
#if defined (HAVE_MDNS)
    md_zeroconf_register(configs);
#elif defined (HAVE_AVAHI)
    av_zeroconf_register(configs);
#endif
}

Three DNS-SD service types are defined in afp_zeroconf.h:

Constant Value Purpose
AFP_DNS_SERVICE_TYPE "_afpovertcp._tcp" AFP file sharing discovery
ADISK_SERVICE_TYPE "_adisk._tcp" Time Machine discovery
DEV_INFO_SERVICE_TYPE "_device-info._tcp" Device model advertisement

Registration occurs after all services are started but before entering the event loop. On SIGHUP, services are deregistered and re-registered to reflect configuration changes.


9. Relationship to afpd

The netatalk controller and afpd are separate processes with distinct responsibilities:

Aspect netatalk afpd
Role Service controller / supervisor AFP protocol server
Source etc/netatalk/netatalk.c etc/afpd/main.c
Event loop libevent2 event_base_dispatch() poll() loop in main()
Child management Manages afpd, cnid_metad, dbus Manages per-session fork children via server_child_alloc()
Config parsing afp_config_parse(&obj, "netatalk") afp_config_parse(&dsi_obj, "afpd")
SIGHUP handling Reloads volumes, re-registers Zeroconf, forwards SIGHUP Sets reloadconfig=1, re-parses afp.conf, reinitializes listening sockets, forwards SIGHUP to child workers

When netatalk launches afpd:

  1. netatalk calls run_process(_PATH_AFPD, "-d", "-F", configfile)
  2. afpd runs in foreground mode (-d), parses its own copy of afp.conf
  3. afpd enters its own poll() loop, accepting DSI/ASP connections and forking session children
  4. If afpd crashes, netatalk’s sigchld_cb() reaps it and sets afpd_pid = NETATALK_SRV_ERROR
  5. On the next 1-second timer tick, timer_cb() restarts afpd

10. Relationship to cnid_metad

cnid_metad is launched by netatalk only when CNID_BACKEND_DBD is compiled in. It acts as a connection broker between afpd workers and per-volume cnid_dbd processes, as documented in the cnid_metad.c file header:

                    via TCP socket
1.       afpd          ------->        cnid_metad

                via UNIX domain socket
2.   cnid_metad        ------->         cnid_dbd

                 passes afpd client fd
3.   cnid_metad        ------->         cnid_dbd

Result:
                    via TCP socket
4.       afpd          ------->         cnid_dbd

This means cnid_metad only brokers the initial connection — subsequent CNID operations go directly between afpd and cnid_dbd.


11. Init System Integration

The netatalk daemon integrates with every major init system via platform-specific scripts in distrib/initscripts/. All init scripts start the same netatalk binary, which then manages child processes internally.

systemd

distrib/initscripts/systemd.netatalk.service.in:

[Unit]
Description=Netatalk AFP fileserver
Documentation=man:afp.conf(5) man:netatalk(8) man:afpd(8) man:cnid_metad(8) man:cnid_dbd(8)
After=network-online.target avahi-daemon.service atalkd.service

[Service]
Type=forking
GuessMainPID=no
ExecStart=@sbindir@/netatalk
PIDFile=@lockfile_path@/netatalk
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=1

[Install]
WantedBy=multi-user.target

Key details: Type=forking because netatalk calls daemonize() (unless -d). GuessMainPID=no with explicit PIDFile. ExecReload sends SIGHUP for live config reload. Restart=always provides systemd-level restart on top of netatalk’s internal child restart.

OpenRC (Gentoo / Alpine)

distrib/initscripts/openrc.netatalk.in:

#!/sbin/openrc-run
extra_started_commands="reload"
name=$RC_SVCNAME
command="@sbindir@/${RC_SVCNAME}"
pidfile=@lockfile_path@

depend() {
    need net
    use logger dns
    after atalkd
    after avahi-daemon
    after firewall
}

reload() {
    ebegin "Reloading $name"
    start-stop-daemon --signal SIGHUP --name $command
    eend $?
}

SysV Init (Debian)

distrib/initscripts/debian.netatalk.in provides LSB-compliant init with start, stop, restart, force-reload, and status commands using start-stop-daemon.

FreeBSD rc.d

distrib/initscripts/freebsd.netatalk.in uses rc.subr with a custom reload command that sends SIGHUP:

netatalk_reload()
{
    echo 'Reloading netatalk.'
    kill -HUP $rc_pid
}

NetBSD rc.d

distrib/initscripts/netbsd.netatalk.in: minimal rc.subr script with required_files="$etcdir/afp.conf".

OpenBSD rc.d

distrib/initscripts/openbsd.netatalk.in: minimal rc.subr script.

macOS launchd

distrib/initscripts/macos.netatalk.plist.in:

<key>Label</key>
<string>io.netatalk.daemon</string>
<key>ProgramArguments</key>
<array>
    <string>@sbindir@/netatalk</string>
    <string>-d</string>
</array>
<key>RunAtLoad</key>
<true/>

Note: macOS uses -d (foreground mode) since launchd manages the process lifecycle. The companion startup script macos.netatalk.in provides StartService, StopService, and RestartService functions, waiting for network availability before launch.

Solaris SMF

distrib/initscripts/solaris.netatalk.xml.in: SMF manifest with network/netatalk service name, dependencies on network and filesystem, and optional mdns dependency. Stop method uses :kill (SIGTERM).

Init System Comparison

Init System Script Reload Method Service Type
systemd systemd.netatalk.service.in kill -HUP $MAINPID Type=forking
OpenRC openrc.netatalk.in start-stop-daemon --signal SIGHUP Standard daemon
SysV debian.netatalk.in force-reload (full restart) start-stop-daemon
FreeBSD rc.d freebsd.netatalk.in kill -HUP $rc_pid rc.subr
NetBSD rc.d netbsd.netatalk.in rc.subr
OpenBSD rc.d openbsd.netatalk.in rc.subr
macOS launchd macos.netatalk.plist.in RunAtLoad, foreground -d
Solaris SMF solaris.netatalk.xml.in duration=contract

12. Build Configuration

The netatalk binary is built from etc/netatalk/meson.build:

netatalk_sources = ['afp_avahi.c', 'afp_mdns.c', 'afp_zeroconf.c', 'netatalk.c']
netatalk_deps = [bstring, iniparser, libevent]

executable(
    'netatalk',
    netatalk_sources,
    include_directories: root_includes,
    link_with: libatalk,
    dependencies: netatalk_deps,
    c_args: [confdir, statedir, afpd, cnid_metad, dversion],
    install: true,
    install_dir: sbindir,
)

The c_args inject compile-time path defines from the top-level meson.build:

Define Value Used For
_PATH_AFPD sbindir/afpd Launching afpd
_PATH_CNID_METAD sbindir/cnid_metad Launching cnid_metad
_PATH_CONFDIR pkgconfdir/ Finding afp.conf, dbus-session.conf
_PATH_STATEDIR localstatedir/netatalk/ D-Bus socket, Tracker state
VERSION netatalk_version Version display
PATH_NETATALK_LOCK lockfile_path/netatalk PID file / lock file

Conditional Compilation Flags

Flag Effect Build Source
CNID_BACKEND_DBD Enables cnid_metad management BDB dependency in meson.build
WITH_SPOTLIGHT Enables D-Bus + indexer management Spotlight option in meson.build
HAVE_AVAHI Selects Avahi for Zeroconf Avahi dependency check
HAVE_MDNS Selects mDNSResponder for Zeroconf DNS-SD dependency check
DBUS_DAEMON_PATH Path to dbus-daemon binary Auto-detected in meson.build
INDEXER_COMMAND Tracker/LocalSearch daemon command Auto-detected in meson.build
INDEXER_DBUS_NAME D-Bus name for indexer service Auto-detected in meson.build

Footnotes

This is a mirror of the Netatalk GitHub Wiki

Last updated 2026-04-06