Dev Docs Netatalk Controller Daemon
- 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:
afpd_pid=NETATALK_SRV_NEEDED— always startedcnid_metad_pid=NETATALK_SRV_NEEDEDifCNID_BACKEND_DBDis compiled in, otherwiseNETATALK_SRV_OPTIONALdbus_pid=NETATALK_SRV_OPTIONAL— only started ifWITH_SPOTLIGHTandOPTION_SPOTLIGHT
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:
netatalkcallsrun_process(_PATH_AFPD, "-d", "-F", configfile)afpdruns in foreground mode (-d), parses its own copy ofafp.confafpdenters its ownpoll()loop, accepting DSI/ASP connections and forking session children- If
afpdcrashes,netatalk’ssigchld_cb()reaps it and setsafpd_pid = NETATALK_SRV_ERROR - On the next 1-second timer tick,
timer_cb()restartsafpd
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