--- /dev/null
+<html>
+<body>
+<h1>Getting Started With Launchd</h1>
+
+<p>Launchd is rather simple actually. Far simpler than you might think.</p>
+
+<h2>Before We Get Started</h2>
+
+<p>Launchd, in an effort to be consistent with other Apple software, uses the
+Property List file format for storing configuration information. Apple's
+Property List APIs provide for three different file formats for storing a
+property list to disk. A plain text option, a binary option, and an XML text
+option. For the remainder of this HOWTO, we will use the XML varient to show
+what a configuration file looks like.</p>
+
+<h3>The basics:</h3>
+
+<p>For the simplest of scenarios, launchd just keeps a process alive. A simple "hello
+world" example of that would be:</p>
+
+<blockquote><pre>
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>com.example.sleep</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>sleep</string>
+ <string>100</string>
+ </array>
+ <key>OnDemand</key>
+ <false/>
+</dict>
+</plist>
+</pre></blockquote>
+
+<p>In the above example, we have three keys to our top level dictionary. The first
+is the <code>Label</code> which is what is used to uniquely identify jobs when interacting
+with launchd. The second is <code>ProgramArguments</code> which for its value, we have an
+array of strings which represent the tokenized arguments and the program to
+run. The third and final key is <code>OnDemand</code> which overrides the default value of
+true with false thereby instructing launchd to always try and keep this job
+running. That's it! A <code>Label</code>, some <code>ProgramArguments</code> and <code>OnDemand</code> set to false is all
+you need to keep a daemon alive with launchd!</p>
+
+<p>Now if you've ever written a daemon before, you've either called the
+<code>daemon()</code> function or written one yourself. With launchd, that is
+not only unnecessary, but unsupported. If you try and run a daemon you didn't
+write under launchd, you must, at the very least, find a configuration option to
+keep the daemon from daemonizing itself so that launchd can monitor it.</p>
+
+<h3>Going beyond the basics with optional keys:</h3>
+
+<p>There are many optional keys available and documented in the launchd.plist
+man page, so we'll only give a few optional, but common examples:</p>
+
+<blockquote><pre>
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>com.example.sleep</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>sleep</string>
+ <string>100</string>
+ </array>
+ <key>OnDemand</key>
+ <false/>
+ <key>UserName</key>
+ <string>daemon</string>
+ <key>GroupName</key>
+ <string>daemon</string>
+ <key>EnvironmentVariables</key>
+ <dict>
+ <key>FOO</key>
+ <string>bar</string>
+ </dict>
+</dict>
+</plist>
+</pre></blockquote>
+
+<p>In the above example, we see that the job is run as a certain user and group, and additionally has an extra environment variable set.</p>
+
+<h3>Debugging tricks:</h3>
+
+<p>The following example will enable core dumps, set standard out and error to
+go to a log file and to instruct launchd to temporarily bump up the debug level
+of launchd's loggging while acting on behave of your job (remember to adjust
+your syslog.conf accordingly):</p>
+
+<blockquote><pre>
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>com.example.sleep</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>sleep</string>
+ <string>100</string>
+ </array>
+ <key>OnDemand</key>
+ <false/>
+ <key>StandardOutPath</key>
+ <string>/var/log/myjob.log</string>
+ <key>StandardErrorPath</key>
+ <string>/var/log/myjob.log</string>
+ <key>Debug</key>
+ <true/>
+ <key>SoftResourceLimits</key>
+ <dict>
+ <key>Core</key>
+ <integer>9223372036854775807</integer>
+ </dict>
+ <key>HardResourceLimits</key>
+ <dict>
+ <key>Core</key>
+ <integer>9223372036854775807</integer>
+ </dict>
+</dict>
+</plist>
+</pre></blockquote>
+
+<h2>But what if I don't want or expect my job to run continuously?</h2>
+
+<h3>The basics of on demand launching:</h3>
+
+<p>Launchd provides a multitude of different criteria that can be used to specify
+when a job should be started. It is important to note that launchd will only
+run one instance of your job though. Therefore, if your on demand job
+malfunctions, you are guaranteed that launchd will not spawn additional copies
+when your criteria is satisfied once more in the future.</p>
+
+<h3>Starting a job periodically:</h3>
+
+<p>Here is an example on how to have a job start every five minutes (300 seconds):</p>
+
+<blockquote><pre>
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>com.example.touchsomefile</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>touch</string>
+ <string>/tmp/helloworld</string>
+ </array>
+ <key>StartInterval</key>
+ <integer>300</integer>
+</dict>
+</plist>
+</pre></blockquote>
+
+<p>Sometimes you want a job started on a calendar based interval. The following example will start the job on the 11th minute of the 11th hour every day (using a 24 hour clock system) on the 11th day of the month. Like the Unix cron subsystem, any missing key of the <code>StartCalendarInterval</code> dictionary is treated as a wildcard:</p>
+
+<blockquote><pre>
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>com.example.touchsomefile</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>touch</string>
+ <string>/tmp/helloworld</string>
+ </array>
+ <key>StartCalendarInterval</key>
+ <dict>
+ <key>Minute</key>
+ <integer>11</integer>
+ <key>Hour</key>
+ <integer>11</integer>
+ <key>Day</key>
+ <integer>11</integer>
+ </dict>
+</dict>
+</pre></blockquote>
+
+<h3>Starting a job based on file system activity:</h3>
+
+<p>The following example will start the job whenever any of the paths being watched change for whatever reason:</p>
+
+<blockquote><pre>
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>com.example.watchetchostconfig</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>syslog</string>
+ <string>-s</string>
+ <string>-l</string>
+ <string>notice</string>
+ <string>somebody touched /etc/hostconfig</string>
+ </array>
+ <key>WatchPaths</key>
+ <array>
+ <string>/etc/hostconfig</string>
+ </array>
+</dict>
+</pre></blockquote>
+
+<p>An additional file system trigger is the notion of a queue directory. Launchd will star your job whenever
+the given directories are non-empty and will keep your job running as long as those directories are not empty:</p>
+
+<blockquote><pre>
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>com.example.mailpush</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>my_custom_mail_push_tool</string>
+ </array>
+ <key>QueueDirectories</key>
+ <array>
+ <string>/var/spool/mymailqdir</string>
+ </array>
+</dict>
+</pre></blockquote>
+
+<h3>Inetd Emulation</h3>
+
+<p>Launchd will happily emulate inetd style daemon semantics:</p>
+
+<blockquote><pre>
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>com.example.telnetd</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>/usr/libexec/telnetd</string>
+ </array>
+ <key>inetdCompatibility</key>
+ <dict>
+ <key>Wait</key>
+ <false/>
+ </dict>
+ <key>Sockets</key>
+ <dict>
+ <key>Listeners</key>
+ <dict>
+ <key>SockServiceName</key>
+ <string>telnet</string>
+ <key>SockType</key>
+ <string>stream</string>
+ </dict>
+ </dict>
+</dict>
+</pre></blockquote>
+
+<h1>TBD</h1>
+
+</body>
+</html>
--- /dev/null
+#include <sys/types.h>
+#include <sys/event.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <syslog.h>
+#include <libgen.h>
+
+#include "launch.h"
+
+int main(void)
+{
+ struct timespec timeout = { 60, 0 };
+ struct sockaddr_storage ss;
+ socklen_t slen = sizeof(ss);
+ struct kevent kev;
+ launch_data_t tmp, resp, msg = launch_data_new_string(LAUNCH_KEY_CHECKIN);
+ size_t i;
+ int kq;
+
+ openlog(getprogname(), LOG_PERROR|LOG_PID|LOG_CONS, LOG_DAEMON);
+
+ if (-1 == (kq = kqueue())) {
+ syslog(LOG_ERR, "kqueue(): %m");
+ exit(EXIT_FAILURE);
+ }
+
+ if ((resp = launch_msg(msg)) == NULL) {
+ syslog(LOG_ERR, "launch_msg(\"" LAUNCH_KEY_CHECKIN "\") IPC failure: %m");
+ exit(EXIT_FAILURE);
+ }
+
+ if (LAUNCH_DATA_ERRNO == launch_data_get_type(resp)) {
+ errno = launch_data_get_errno(resp);
+ syslog(LOG_ERR, "Check-in failed: %m");
+ exit(EXIT_FAILURE);
+ }
+
+ tmp = launch_data_dict_lookup(resp, LAUNCH_JOBKEY_TIMEOUT);
+ if (tmp)
+ timeout.tv_sec = launch_data_get_integer(tmp);
+
+ tmp = launch_data_dict_lookup(resp, LAUNCH_JOBKEY_SOCKETS);
+ if (NULL == tmp) {
+ syslog(LOG_ERR, "No sockets found to answer requests on!");
+ exit(EXIT_FAILURE);
+ }
+
+ if (launch_data_dict_get_count(tmp) > 1) {
+ syslog(LOG_WARNING, "Some sockets will be ignored!");
+ }
+
+ tmp = launch_data_dict_lookup(tmp, "SampleListeners");
+ if (NULL == tmp) {
+ syslog(LOG_ERR, "No known sockets found to answer requests on!");
+ exit(EXIT_FAILURE);
+ }
+
+ for (i = 0; i < launch_data_array_get_count(tmp); i++) {
+ launch_data_t tmpi = launch_data_array_get_index(tmp, i);
+
+ EV_SET(&kev, launch_data_get_fd(tmpi), EVFILT_READ, EV_ADD, 0, 0, NULL);
+ if (kevent(kq, &kev, 1, NULL, 0, NULL) == -1) {
+ syslog(LOG_DEBUG, "kevent(): %m");
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ launch_data_free(msg);
+ launch_data_free(resp);
+
+ for (;;) {
+ FILE *c;
+ int r;
+
+ if ((r = kevent(kq, NULL, 0, &kev, 1, &timeout)) == -1) {
+ syslog(LOG_ERR, "kevent(): %m");
+ exit(EXIT_FAILURE);
+ } else if (r == 0) {
+ exit(EXIT_SUCCESS);
+ }
+
+ if ((r = accept(kev.ident, (struct sockaddr *)&ss, &slen)) == -1) {
+ syslog(LOG_ERR, "accept(): %m");
+ continue; /* this isn't fatal */
+ }
+
+ c = fdopen(r, "r+");
+
+ if (c) {
+ fprintf(c, "hello world!\n");
+ fclose(c);
+ } else {
+ close(r);
+ }
+ }
+}
/* Compute path to excecutable */
{
char *tmp;
- strcpy(anExecutable, aBundlePath); /* .../foo */
+ strncpy(anExecutable, aBundlePath, sizeof(anExecutable)); /* .../foo */
tmp = rindex(anExecutable, '/'); /* /foo */
strncat(anExecutable, tmp, strlen(tmp)); /* .../foo/foo */
}
#define LAUNCH_JOBSOCKETKEY_SERVICENAME "SockServiceName"
#define LAUNCH_JOBSOCKETKEY_FAMILY "SockFamily"
#define LAUNCH_JOBSOCKETKEY_PROTOCOL "SockProtocol"
-#define LAUNCH_JOBSOCKETKEY_FD "SockFD"
-#define LAUNCH_JOBSOCKETKEY_ADDRINFORESULTS "AddrinfoResults"
+#define LAUNCH_JOBSOCKETKEY_MULTICASTGROUP "MulticastGroup"
typedef struct _launch_data *launch_data_t;
starts.
.Sh SUBCOMMANDS
.Bl -tag -width -indent
-.It Xo Ar load Op Fl w
+.It Xo Ar load Op Fl wF
.Ar paths ...
.Xc
Load the specified configuration files or directories of configuration files.
.Bl -tag -width -indent
.It Fl w
Remove the disabled key and write the configuration files back out to disk.
+.It Fl F
+Force the loading of the plist. Ignore the Disabled key.
.El
.It Xo Ar unload Op Fl w
.Ar paths ...
.It Ar help
Print out a quick usage statement.
.El
+.Sh ENVIRONMENTAL VARIABLES
+.Bl -tag -width -indent
+.It Pa LAUNCHD_SOCKET
+This variable informs launchctl how to find the correct launchd to talk to. If it is missing, launchctl will use a built-in default.
+.El
.Sh FILES
.Bl -tag -width "/System/Library/LaunchDaemons" -compact
.It Pa ~/Library/LaunchAgents
static launch_data_t read_plist_file(const char *file, bool editondisk, bool load);
static CFPropertyListRef CreateMyPropertyListFromFile(const char *);
static void WriteMyPropertyListToFile(CFPropertyListRef, const char *);
-static void readpath(const char *, launch_data_t, launch_data_t, bool editondisk, bool load);
+static void readpath(const char *, launch_data_t, launch_data_t, bool editondisk, bool load, bool forceload);
static int _fd(int);
static int demux_cmd(int argc, char *const argv[]);
static launch_data_t do_rendezvous_magic(const struct addrinfo *res, const char *serv);
static void submit_job_pass(launch_data_t jobs);
+static void do_mgroup_join(int fd, int family, int socktype, int protocol, const char *mgroup);
static int load_and_unload_cmd(int argc, char *const argv[]);
//static int reload_cmd(int argc, char *const argv[]);
return res;
}
-static void readfile(const char *what, launch_data_t pass1, launch_data_t pass2, bool editondisk, bool load)
+static void readfile(const char *what, launch_data_t pass1, launch_data_t pass2, bool editondisk, bool load, bool forceload)
{
launch_data_t tmpd, thejob;
bool job_disabled = false;
return;
}
+ if (NULL == launch_data_dict_lookup(thejob, LAUNCH_JOBKEY_LABEL)) {
+ fprintf(stderr, "%s: missing the Label key: %s\n", getprogname(), what);
+ launch_data_free(thejob);
+ return;
+ }
+
if ((tmpd = launch_data_dict_lookup(thejob, LAUNCH_JOBKEY_DISABLED)))
job_disabled = launch_data_get_bool(tmpd);
+ if (forceload)
+ job_disabled = false;
+
if (job_disabled && load) {
launch_data_free(thejob);
return;
launch_data_array_append(pass1, thejob);
}
-static void readpath(const char *what, launch_data_t pass1, launch_data_t pass2, bool editondisk, bool load)
+static void readpath(const char *what, launch_data_t pass1, launch_data_t pass2, bool editondisk, bool load, bool forceload)
{
char buf[MAXPATHLEN];
struct stat sb;
return;
if (S_ISREG(sb.st_mode) && !(sb.st_mode & S_IWOTH)) {
- readfile(what, pass1, pass2, editondisk, load);
+ readfile(what, pass1, pass2, editondisk, load, forceload);
} else {
if ((d = opendir(what)) == NULL) {
fprintf(stderr, "%s: opendir() failed to open the directory\n", getprogname());
continue;
snprintf(buf, sizeof(buf), "%s/%s", what, de->d_name);
- readfile(buf, pass1, pass2, editondisk, load);
+ readfile(buf, pass1, pass2, editondisk, load, forceload);
}
closedir(d);
}
launch_data_array_append(fdarray, val);
} else {
launch_data_t rnames = NULL;
- const char *node = NULL, *serv = NULL;
+ const char *node = NULL, *serv = NULL, *mgroup = NULL;
char servnbuf[50];
struct addrinfo hints, *res0, *res;
int gerr, sock_opt = 1;
if ((val = launch_data_dict_lookup(tmp, LAUNCH_JOBSOCKETKEY_NODENAME)))
node = launch_data_get_string(val);
+ if ((val = launch_data_dict_lookup(tmp, LAUNCH_JOBSOCKETKEY_MULTICASTGROUP)))
+ mgroup = launch_data_get_string(val);
if ((val = launch_data_dict_lookup(tmp, LAUNCH_JOBSOCKETKEY_SERVICENAME))) {
if (LAUNCH_DATA_INTEGER == launch_data_get_type(val)) {
sprintf(servnbuf, "%lld", launch_data_get_integer(val));
fprintf(stderr, "setsockopt(IPV6_V6ONLY): %m");
return;
}
- if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, (void *)&sock_opt, sizeof(sock_opt)) == -1) {
- fprintf(stderr, "socket(): %s\n", strerror(errno));
- return;
+ if (mgroup) {
+ if (setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, (void *)&sock_opt, sizeof(sock_opt)) == -1) {
+ fprintf(stderr, "setsockopt(SO_REUSEPORT): %s\n", strerror(errno));
+ return;
+ }
+ } else {
+ if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, (void *)&sock_opt, sizeof(sock_opt)) == -1) {
+ fprintf(stderr, "setsockopt(SO_REUSEADDR): %s\n", strerror(errno));
+ return;
+ }
}
if (bind(sfd, res->ai_addr, res->ai_addrlen) == -1) {
fprintf(stderr, "bind(): %s\n", strerror(errno));
return;
}
+
+ if (mgroup) {
+ do_mgroup_join(sfd, res->ai_family, res->ai_socktype, res->ai_protocol, mgroup);
+ }
if ((res->ai_socktype == SOCK_STREAM || res->ai_socktype == SOCK_SEQPACKET)
&& listen(sfd, SOMAXCONN) == -1) {
fprintf(stderr, "listen(): %s\n", strerror(errno));
}
}
+static void do_mgroup_join(int fd, int family, int socktype, int protocol, const char *mgroup)
+{
+ struct addrinfo hints, *res0, *res;
+ struct ip_mreq mreq;
+ struct ipv6_mreq m6req;
+ int gerr;
+
+ memset(&hints, 0, sizeof(hints));
+
+ hints.ai_flags |= AI_PASSIVE;
+ hints.ai_family = family;
+ hints.ai_socktype = socktype;
+ hints.ai_protocol = protocol;
+
+ if ((gerr = getaddrinfo(mgroup, NULL, &hints, &res0)) != 0) {
+ fprintf(stderr, "getaddrinfo(): %s\n", gai_strerror(gerr));
+ return;
+ }
+
+ for (res = res0; res; res = res->ai_next) {
+ if (AF_INET == family) {
+ memset(&mreq, 0, sizeof(mreq));
+ mreq.imr_multiaddr = ((struct sockaddr_in *)res->ai_addr)->sin_addr;
+ if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == -1) {
+ fprintf(stderr, "setsockopt(IP_ADD_MEMBERSHIP): %s\n", strerror(errno));
+ continue;
+ }
+ break;
+ } else if (AF_INET6 == family) {
+ memset(&m6req, 0, sizeof(m6req));
+ m6req.ipv6mr_multiaddr = ((struct sockaddr_in6 *)res->ai_addr)->sin6_addr;
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &m6req, sizeof(m6req)) == -1) {
+ fprintf(stderr, "setsockopt(IPV6_JOIN_GROUP): %s\n", strerror(errno));
+ continue;
+ }
+ break;
+ } else {
+ fprintf(stderr, "unknown family during multicast group bind!\n");
+ break;
+ }
+ }
+
+ freeaddrinfo(res0);
+}
+
+
static launch_data_t do_rendezvous_magic(const struct addrinfo *res, const char *serv)
{
struct stat sb;
int i, ch;
bool wflag = false;
bool lflag = false;
+ bool Fflag = false;
if (!strcmp(argv[0], "load"))
lflag = true;
- while ((ch = getopt(argc, argv, "w")) != -1) {
+ while ((ch = getopt(argc, argv, "wF")) != -1) {
switch (ch) {
- case 'w':
- wflag = true;
- break;
+ case 'w': wflag = true; break;
+ case 'F': Fflag = true; break;
default:
- fprintf(stderr, "usage: %s load [-w] paths...\n", getprogname());
+ fprintf(stderr, "usage: %s load [-wF] paths...\n", getprogname());
return 1;
}
}
pass2 = launch_data_alloc(LAUNCH_DATA_ARRAY);
for (i = 0; i < argc; i++)
- readpath(argv[i], pass1, pass2, wflag, lflag);
+ readpath(argv[i], pass1, pass2, wflag, lflag, Fflag);
if (0 == launch_data_array_get_count(pass1) && 0 == launch_data_array_get_count(pass2)) {
fprintf(stderr, "nothing found to %s\n", lflag ? "load" : "unload");
.Nd System wide and per-user daemon/agent manager
.Sh SYNOPSIS
.Nm
+.Op Fl d
.Op Fl v
.Op Fl s
.Op Fl x
is invoked by the kernel to run as the first process on the system and to further bootstrap the rest of the system.
.Sh OPTIONS WHEN RUN AS PID 1
.Bl -tag -width -indent
+.It Fl d
+Daemonize. Useful when passing a command to launchd on the command line.
.It Fl v
Verbose boot.
.It Fl s
.It Fl x
Safe mode boot. Instructs the system to boot conservatively.
.El
+.Sh ENVIRONMENTAL VARIABLES
+.Bl -tag -width -indent
+.It Pa LAUNCHD_SOCKET
+This variable is exported when invoking a command via the launchd command line. It informs launchctl how to find the correct launchd to talk to.
+.El
.Sh NOTES
In Darwin it is preferable to have your daemon launch via launchd instead of modifying
.Nm rc
static size_t total_children = 0;
static pid_t readcfg_pid = 0;
+static pid_t launchd_proper_pid = 0;
static bool launchd_inited = false;
static bool shutdown_in_progress = false;
static pthread_t mach_server_loop_thread;
static void launchd_clean_up(void)
{
+ if (launchd_proper_pid != getpid())
+ return;
+
seteuid(0);
setegid(0);
goto out_bad;
}
}
+
if (chown(ourdir, getuid(), getgid()) == -1)
syslog(LOG_WARNING, "chown(\"%s\"): %m", ourdir);
+ setegid(getgid());
+ seteuid(getuid());
+
ourdirfd = _fd(open(ourdir, O_RDONLY));
if (ourdirfd == -1) {
syslog(LOG_ERR, "open(\"%s\"): %m", ourdir);
syslog(LOG_ERR, "bind(\"thesocket\"): %m");
goto out_bad;
}
- if (chown(sun.sun_path, getuid(), getgid()) == -1)
- syslog(LOG_WARNING, "chown(\"thesocket\"): %m");
if (listen(fd, SOMAXCONN) == -1) {
syslog(LOG_ERR, "listen(\"thesocket\"): %m");
sockdir = strdup(ourdir);
sockpath = strdup(sun.sun_path);
+ launchd_proper_pid = getpid();
atexit(launchd_clean_up);
out_bad:
if (-1 == (dcc_r = dir_has_files(thepath))) {
job_log_error(j, LOG_ERR, "dir_has_files(\"%s\", ...)", thepath);
- } else if (dcc_r > 0) {
+ } else if (dcc_r > 0 && !shutdown_in_progress) {
job_start(j);
break;
}
launch_data_t tmp, resp;
const char *label;
struct jobcb *j;
- bool startnow;
+ bool startnow, hasprog = false, hasprogargs = false;
if ((label = job_get_string(pload, LAUNCH_JOBKEY_LABEL))) {
TAILQ_FOREACH(j, &jobs, tqe) {
resp = launch_data_new_errno(EINVAL);
goto out;
}
- if (launch_data_dict_lookup(pload, LAUNCH_JOBKEY_PROGRAMARGUMENTS) == NULL) {
+
+ if (launch_data_dict_lookup(pload, LAUNCH_JOBKEY_PROGRAM))
+ hasprog = true;
+ if (launch_data_dict_lookup(pload, LAUNCH_JOBKEY_PROGRAMARGUMENTS))
+ hasprogargs = true;
+
+ if (!hasprog && !hasprogargs) {
resp = launch_data_new_errno(EINVAL);
goto out;
}
launch_data_t tmp_k;
j->start_cal_interval = calloc(1, sizeof(struct tm));
+ j->start_cal_interval->tm_min = -1;
+ j->start_cal_interval->tm_hour = -1;
+ j->start_cal_interval->tm_mday = -1;
+ j->start_cal_interval->tm_wday = -1;
+ j->start_cal_interval->tm_mon = -1;
if (LAUNCH_DATA_DICTIONARY == launch_data_get_type(tmp)) {
if ((tmp_k = launch_data_dict_lookup(tmp, LAUNCH_JOBKEY_CAL_MINUTE)))
j->start_cal_interval->tm_hour = launch_data_get_integer(tmp_k);
if ((tmp_k = launch_data_dict_lookup(tmp, LAUNCH_JOBKEY_CAL_DAY)))
j->start_cal_interval->tm_mday = launch_data_get_integer(tmp_k);
- if ((tmp_k = launch_data_dict_lookup(tmp, LAUNCH_JOBKEY_CAL_WEEKDAY))) {
+ if ((tmp_k = launch_data_dict_lookup(tmp, LAUNCH_JOBKEY_CAL_WEEKDAY)))
j->start_cal_interval->tm_wday = launch_data_get_integer(tmp_k);
- if (j->start_cal_interval->tm_wday == 0)
- j->start_cal_interval->tm_wday = 7;
- }
if ((tmp_k = launch_data_dict_lookup(tmp, LAUNCH_JOBKEY_CAL_MONTH)))
j->start_cal_interval->tm_mon = launch_data_get_integer(tmp_k);
}
job_log(j, LOG_WARNING, "failed to checkin");
job_remove(j);
return false;
+ } else if (j->failed_exits >= LAUNCHD_FAILED_EXITS_THRESHOLD) {
+ job_log(j, LOG_WARNING, "too many failures in succession");
+ job_remove(j);
+ return false;
} else if (od || shutdown_in_progress) {
if (!od && shutdown_in_progress)
job_log(j, LOG_NOTICE, "exited while shutdown is in progress, will not restart unless demand requires it");
job_watch(j);
return false;
- } else if (j->failed_exits >= LAUNCHD_FAILED_EXITS_THRESHOLD) {
- job_log(j, LOG_WARNING, "too many failures in a row for a job that should be alive all the time");
- job_remove(j);
- return false;
}
return true;
startnow = false;
}
}
- } else if (kev->filter == EVFILT_TIMER && kev->fflags & NOTE_ABSOLUTE) {
- job_set_alarm(j);
+ } else if (kev->filter == EVFILT_TIMER && (void *)kev->ident == j->start_cal_interval) {
+ job_set_alarm(j);
} else if (kev->filter == EVFILT_VNODE) {
size_t i;
const char *thepath = NULL;
bool inetcompat = job_get_bool(j->ldj, LAUNCH_JOBKEY_INETDCOMPATIBILITY);
size_t i, argv_cnt;
const char **argv, *file2exec = "/usr/libexec/launchproxy";
+ int r;
+ bool hasprog = false;
job_setup_attributes(j);
- argv_cnt = launch_data_array_get_count(ldpa);
- argv = alloca((argv_cnt + 2) * sizeof(char *));
- for (i = 0; i < argv_cnt; i++)
- argv[i + 1] = launch_data_get_string(launch_data_array_get_index(ldpa, i));
- argv[argv_cnt + 1] = NULL;
+ if (ldpa) {
+ argv_cnt = launch_data_array_get_count(ldpa);
+ argv = alloca((argv_cnt + 2) * sizeof(char *));
+ for (i = 0; i < argv_cnt; i++)
+ argv[i + 1] = launch_data_get_string(launch_data_array_get_index(ldpa, i));
+ argv[argv_cnt + 1] = NULL;
+ } else {
+ argv = alloca(3 * sizeof(char *));
+ argv[1] = job_get_string(j->ldj, LAUNCH_JOBKEY_PROGRAM);
+ argv[2] = NULL;
+ }
+
+ if (job_get_string(j->ldj, LAUNCH_JOBKEY_PROGRAM))
+ hasprog = true;
if (inetcompat) {
argv[0] = file2exec;
file2exec = job_get_file2exec(j->ldj);
}
- if (-1 == execvp(file2exec, (char *const*)argv)) {
- int e = errno; /* errno is a macro that expands, best not to take the address of it */
- write(execfd, &e, sizeof(e));
- job_log_error(j, LOG_ERR, "execvp(\"%s\", ...)", file2exec);
+ if (hasprog) {
+ r = execv(file2exec, (char *const*)argv);
+ } else {
+ r = execvp(file2exec, (char *const*)argv);
+ }
+
+ if (-1 == r) {
+ write(execfd, &errno, sizeof(errno));
+ job_log_error(j, LOG_ERR, "execv%s(\"%s\", ...)", hasprog ? "" : "p", file2exec);
}
- _exit(EXIT_FAILURE);
+ exit(EXIT_FAILURE);
}
static void job_setup_attributes(struct jobcb *j)
gre_g = gre->gr_gid;
if (-1 == setgid(gre_g)) {
job_log_error(j, LOG_ERR, "setgid(%d)", gre_g);
- _exit(EXIT_FAILURE);
+ exit(EXIT_FAILURE);
}
} else {
job_log(j, LOG_ERR, "getgrnam(\"%s\") failed", tmpstr);
- _exit(EXIT_FAILURE);
+ exit(EXIT_FAILURE);
}
}
if ((tmpstr = job_get_string(j->ldj, LAUNCH_JOBKEY_USERNAME))) {
if (pwe->pw_expire && time(NULL) >= pwe->pw_expire) {
job_log(j, LOG_ERR, "expired account: %s", tmpstr);
- _exit(EXIT_FAILURE);
+ exit(EXIT_FAILURE);
}
if (job_get_bool(j->ldj, LAUNCH_JOBKEY_INITGROUPS)) {
if (-1 == initgroups(tmpstr, gre ? gre_g : pwe_g)) {
job_log_error(j, LOG_ERR, "initgroups()");
- _exit(EXIT_FAILURE);
+ exit(EXIT_FAILURE);
}
}
if (!gre) {
if (-1 == setgid(pwe_g)) {
job_log_error(j, LOG_ERR, "setgid(%d)", pwe_g);
- _exit(EXIT_FAILURE);
+ exit(EXIT_FAILURE);
}
}
if (-1 == setuid(pwe_u)) {
job_log_error(j, LOG_ERR, "setuid(%d)", pwe_u);
- _exit(EXIT_FAILURE);
+ exit(EXIT_FAILURE);
}
} else {
job_log(j, LOG_WARNING, "getpwnam(\"%s\") failed", tmpstr);
- _exit(EXIT_FAILURE);
+ exit(EXIT_FAILURE);
}
}
if ((tmpstr = job_get_string(j->ldj, LAUNCH_JOBKEY_WORKINGDIRECTORY)))
int fd = open(ldconf, O_RDONLY);
if (fd == -1) {
syslog(LOG_ERR, "open(\"%s\"): %m", ldconf);
- _exit(EXIT_FAILURE);
+ exit(EXIT_FAILURE);
}
dup2(fd, STDIN_FILENO);
close(fd);
execl(LAUNCHCTL_PATH, LAUNCHCTL_PATH, NULL);
syslog(LOG_ERR, "execl(\"%s\", ...): %m", LAUNCHCTL_PATH);
- _exit(EXIT_FAILURE);
+ exit(EXIT_FAILURE);
} else if (readcfg_pid == -1) {
close(spair[0]);
close(spair[1]);
latertm.tm_isdst = -1;
- if (j->start_cal_interval->tm_min)
+ if (-1 != j->start_cal_interval->tm_min)
latertm.tm_min = j->start_cal_interval->tm_min;
- if (j->start_cal_interval->tm_hour)
+ if (-1 != j->start_cal_interval->tm_hour)
latertm.tm_hour = j->start_cal_interval->tm_hour;
otherlatertm = latertm;
- if (j->start_cal_interval->tm_mday)
+ if (-1 != j->start_cal_interval->tm_mday)
latertm.tm_mday = j->start_cal_interval->tm_mday;
- if (j->start_cal_interval->tm_mon)
+ if (-1 != j->start_cal_interval->tm_mon)
latertm.tm_mon = j->start_cal_interval->tm_mon;
/* cron semantics are fun */
- if (j->start_cal_interval->tm_wday) {
+ if (-1 != j->start_cal_interval->tm_wday) {
int delta, realwday = j->start_cal_interval->tm_wday;
if (realwday == 7)
otherlatertm.tm_mday += delta;
else if (delta < 0)
otherlatertm.tm_mday += 7 + delta;
- else if (otherlatertm.tm_hour < nowtm->tm_hour)
+ else if (-1 != j->start_cal_interval->tm_hour && otherlatertm.tm_hour <= nowtm->tm_hour)
otherlatertm.tm_mday += 7;
- else if (otherlatertm.tm_min < nowtm->tm_min)
+ else if (-1 != j->start_cal_interval->tm_min && otherlatertm.tm_min <= nowtm->tm_min)
otherlatertm.tm_hour++;
else
otherlatertm.tm_min++;
otherlater = mktime(&otherlatertm);
}
- if (latertm.tm_mon < nowtm->tm_mon) {
+ if (-1 != j->start_cal_interval->tm_mon && latertm.tm_mon <= nowtm->tm_mon) {
latertm.tm_year++;
- } else if (latertm.tm_mday < nowtm->tm_mday) {
+ } else if (-1 != j->start_cal_interval->tm_mday && latertm.tm_mday <= nowtm->tm_mday) {
latertm.tm_mon++;
- } else if (latertm.tm_hour < nowtm->tm_hour) {
+ } else if (-1 != j->start_cal_interval->tm_hour && latertm.tm_hour <= nowtm->tm_hour) {
latertm.tm_mday++;
- } else if (latertm.tm_min < nowtm->tm_min) {
+ } else if (-1 != j->start_cal_interval->tm_min && latertm.tm_min <= nowtm->tm_min) {
latertm.tm_hour++;
} else {
latertm.tm_min++;
later = mktime(&latertm);
if (otherlater) {
- if (j->start_cal_interval->tm_mday)
+ if (-1 != j->start_cal_interval->tm_mday)
later = later < otherlater ? later : otherlater;
else
later = otherlater;
.It Sy inetdCompatibility <dictionary>
The presence of this key specifies that the daemon expects to be run as if it were launched from
.Xr inetd 8 .
+This flag is incompatible with the ServiceIPC key.
.Bl -ohang -offset indent
.It Sy Wait <boolean>
This flag corresponds to the "wait" or "nowait" option of
.It Sy ServiceIPC <boolean>
This optional key specifies whether the job participates in advanced communication with
.Nm launchd .
-The default is false.
+The default is false. This flag is incompatible with the inetdCompatibility key.
.It Sy TimeOut <integer>
The recommended time out to pass to the job. If no value is specified, a default time out will be supplied by
.Nm launchd
.It Sy InitGroups <boolean>
This optional key specifies whether the job should have
.Xr initgroups 3
-should be called before running the job.
+be called before running the job.
The default is false.
.It Sy WatchPaths <array of strings>
This optional key causes the job to be started if any one of the listed paths are modified.
Much like the WatchPaths option, this key will watch the paths for modifications. The difference being that the job will only be started if the path is a directory and the directory is not empty.
.It Sy StartInterval <integer>
This optional key causes the job to be started every N seconds.
+If the system is asleep, the job will be started the next time the computer
+wakes up. If multiple intervals transpire before the computer is woken, those
+events will be coalesced into one event upon wake from sleep.
.It Sy StartCalendarInterval <dictionary of integers>
This optional key causes the job to be started every calendar interval as specified. Missing arguments are considered to be wildcard. The semantics are much like
-.Xr crontab 5
-.
+.Xr crontab 5 .
+Unlike cron which skips job invocations when the computer is asleep, launchd
+will start the job the next time the computer wakes up. If multiple intervals
+transpire before the computer is woken, those events will be coalesced into one
+event upon wake from sleep.
.Bl -ohang -offset indent
.It Sy Minute <integer>
The minute on which this job will be run.
This optional key specifies that
.Nm launchd
should adjust its log mask temporarily to LOG_DEBUG while dealing with this job.
-.Xr stdio 3 .
.It Sy SoftResourceLimits <dictionary of integers>
.It Sy HardResourceLimits <dictionary of integers>
Resource limits to be imposed on the job. These adjust variables set with
value should be applied to the daemon.
.It Sy LowPriorityIO <boolean>
This optional key specifies whether the kernel should consider this daemon to be low priority when doing file system I/O.
-.It Sy Sockets <dictionary of dictionaries...>
+.It Sy Sockets <dictionary of dictionaries... OR dictionary of array of dictionaries...>
This optional key is used to specify launch on demand sockets that can be used to let
.Nm launchd
know when to run the job. The job can check-in and get a copy of the file descriptors using APIs outlined in
.Xr launch 3 .
+The keys of the top level Sockets dictionary can be anything. They are meant for the application developer to use to
+differentiate different which descriptors correspond to which application level protocols (e.g. http vs. ftp vs. DNS...).
+At check in time, the value of each Sockets dictionary key will be an array of descriptors. Daemon/Agent writers should
+consider all descriptors of a given key to be to be effectively equivalent, even though each file descriptor likely represents
+a different networking protocol which conforms to the criteria specified in the job configuration file.
+.Pp
The paramters below are used as inputs to call
.Xr getaddrinfo 3 .
.Bl -ohang -offset indent
This optional key can be used to request that the service be registered with the
.Xr mDNSResponder 8 .
If the value is boolean, the service name is inferred from the SockServiceName.
+.It Sy MulticastGroup <string>
+This optional key can be used to request that the datagram socket join a multicast group.
+If the value is a hostname, then
+.Xr getaddrinfo 3
+will be used to join the correct multicast address for a given socket family.
+If an explicit IPv4 or IPv6 address is given, it is required that the SockFamily family also be set, otherwise the results are undefined.
.El
.El
.Pp
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>com.apple.launchd.test.StartCalendarInterval</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>echo</string>
+ <string>StartCalendarInterval</string>
+ </array>
+ <key>StartCalendarInterval</key>
+ <dict>
+ </dict>
+</dict>
+</plist>