From ab36757d38bc3680d699f3d9d5c21650195fc635 Mon Sep 17 00:00:00 2001 From: Apple Date: Fri, 3 Jun 2005 04:53:59 +0000 Subject: [PATCH] launchd-106.3.tar.gz --- launchd/doc/HOWTO.html | 270 ++++++++++++++++++++ launchd/doc/sampled.c | 103 ++++++++ launchd/src/StartupItems.c | 2 +- launchd/src/launch.h | 3 +- launchd/src/launchctl.1 | 9 +- launchd/src/launchctl.c | 99 +++++-- launchd/src/launchd.8 | 8 + launchd/src/launchd.c | 123 +++++---- launchd/src/launchd.plist.5 | 30 ++- launchd/testing/StartCalendarInterval.plist | 16 ++ 10 files changed, 592 insertions(+), 71 deletions(-) create mode 100644 launchd/doc/HOWTO.html create mode 100644 launchd/doc/sampled.c create mode 100644 launchd/testing/StartCalendarInterval.plist diff --git a/launchd/doc/HOWTO.html b/launchd/doc/HOWTO.html new file mode 100644 index 0000000..a166903 --- /dev/null +++ b/launchd/doc/HOWTO.html @@ -0,0 +1,270 @@ + + +

Getting Started With Launchd

+ +

Launchd is rather simple actually. Far simpler than you might think.

+ +

Before We Get Started

+ +

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.

+ +

The basics:

+ +

For the simplest of scenarios, launchd just keeps a process alive. A simple "hello +world" example of that would be:

+ +
+<?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>
+
+ +

In the above example, we have three keys to our top level dictionary. The first +is the Label which is what is used to uniquely identify jobs when interacting +with launchd. The second is ProgramArguments 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 OnDemand which overrides the default value of +true with false thereby instructing launchd to always try and keep this job +running. That's it! A Label, some ProgramArguments and OnDemand set to false is all +you need to keep a daemon alive with launchd!

+ +

Now if you've ever written a daemon before, you've either called the +daemon() 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.

+ +

Going beyond the basics with optional keys:

+ +

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:

+ +
+<?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>
+
+ +

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.

+ +

Debugging tricks:

+ +

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):

+ +
+<?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>
+
+ +

But what if I don't want or expect my job to run continuously?

+ +

The basics of on demand launching:

+ +

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.

+ +

Starting a job periodically:

+ +

Here is an example on how to have a job start every five minutes (300 seconds):

+ +
+<?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>
+
+ +

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 StartCalendarInterval dictionary is treated as a wildcard:

+ +
+<?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>
+
+ +

Starting a job based on file system activity:

+ +

The following example will start the job whenever any of the paths being watched change for whatever reason:

+ +
+<?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>
+
+ +

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:

+ +
+<?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>
+
+ +

Inetd Emulation

+ +

Launchd will happily emulate inetd style daemon semantics:

+ +
+<?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>
+
+ +

TBD

+ + + diff --git a/launchd/doc/sampled.c b/launchd/doc/sampled.c new file mode 100644 index 0000000..eed5526 --- /dev/null +++ b/launchd/doc/sampled.c @@ -0,0 +1,103 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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); + } + } +} diff --git a/launchd/src/StartupItems.c b/launchd/src/StartupItems.c index 46f0e1a..615cf60 100644 --- a/launchd/src/StartupItems.c +++ b/launchd/src/StartupItems.c @@ -968,7 +968,7 @@ StartupItemRun(CFMutableDictionaryRef aStatusDict, CFMutableDictionaryRef anItem /* 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 */ } diff --git a/launchd/src/launch.h b/launchd/src/launch.h index b4d9557..0a8a175 100644 --- a/launchd/src/launch.h +++ b/launchd/src/launch.h @@ -98,8 +98,7 @@ #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; diff --git a/launchd/src/launchctl.1 b/launchd/src/launchctl.1 index 6049792..8a45e85 100644 --- a/launchd/src/launchctl.1 +++ b/launchd/src/launchctl.1 @@ -24,13 +24,15 @@ to be read at the time 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 ... @@ -107,6 +109,11 @@ of .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 diff --git a/launchd/src/launchctl.c b/launchd/src/launchctl.c index f6911d5..5142250 100644 --- a/launchd/src/launchctl.c +++ b/launchd/src/launchctl.c @@ -58,11 +58,12 @@ static launch_data_t CF2launch_data(CFTypeRef); 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[]); @@ -347,7 +348,7 @@ static bool delay_to_second_pass(launch_data_t o) 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; @@ -357,9 +358,18 @@ static void readfile(const char *what, launch_data_t pass1, launch_data_t pass2, 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; @@ -371,7 +381,7 @@ static void readfile(const char *what, launch_data_t pass1, launch_data_t pass2, 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; @@ -382,7 +392,7 @@ static void readpath(const char *what, launch_data_t pass1, launch_data_t pass2, 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()); @@ -394,7 +404,7 @@ static void readpath(const char *what, launch_data_t pass1, launch_data_t pass2, 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); } @@ -510,7 +520,7 @@ static void sock_dict_edit_entry(launch_data_t tmp, const char *key, launch_data 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; @@ -524,6 +534,8 @@ static void sock_dict_edit_entry(launch_data_t tmp, const char *key, launch_data 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)); @@ -567,14 +579,25 @@ static void sock_dict_edit_entry(launch_data_t tmp, const char *key, launch_data 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)); @@ -624,6 +647,52 @@ static void sock_dict_edit_entry(launch_data_t tmp, const char *key, launch_data } } +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; @@ -801,17 +870,17 @@ static int load_and_unload_cmd(int argc, char *const argv[]) 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; } } @@ -835,7 +904,7 @@ static int load_and_unload_cmd(int argc, char *const argv[]) 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"); diff --git a/launchd/src/launchd.8 b/launchd/src/launchd.8 index e1d3e51..5938eae 100644 --- a/launchd/src/launchd.8 +++ b/launchd/src/launchd.8 @@ -6,6 +6,7 @@ .Nd System wide and per-user daemon/agent manager .Sh SYNOPSIS .Nm +.Op Fl d .Op Fl v .Op Fl s .Op Fl x @@ -31,6 +32,8 @@ During boot 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 @@ -40,6 +43,11 @@ to give a shell prompt before booting the system. .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 diff --git a/launchd/src/launchd.c b/launchd/src/launchd.c index 7078394..0eb441b 100644 --- a/launchd/src/launchd.c +++ b/launchd/src/launchd.c @@ -177,6 +177,7 @@ static void unsetup_job_env(launch_data_t obj, const char *key, void *context); 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; @@ -421,6 +422,9 @@ static char *sockpath = NULL; static void launchd_clean_up(void) { + if (launchd_proper_pid != getpid()) + return; + seteuid(0); setegid(0); @@ -489,9 +493,13 @@ static void launchd_server_init(bool create_session) 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); @@ -524,8 +532,6 @@ static void launchd_server_init(bool create_session) 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"); @@ -542,6 +548,7 @@ static void launchd_server_init(bool create_session) sockdir = strdup(ourdir); sockpath = strdup(sun.sun_path); + launchd_proper_pid = getpid(); atexit(launchd_clean_up); out_bad: @@ -807,7 +814,7 @@ static void job_watch(struct jobcb *j) 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; } @@ -1065,7 +1072,7 @@ static launch_data_t load_job(launch_data_t pload) 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) { @@ -1078,7 +1085,13 @@ static launch_data_t load_job(launch_data_t pload) 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; } @@ -1133,6 +1146,11 @@ static launch_data_t load_job(launch_data_t pload) 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))) @@ -1141,11 +1159,8 @@ static launch_data_t load_job(launch_data_t pload) 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); } @@ -1425,15 +1440,15 @@ static bool job_restart_fitness_test(struct jobcb *j) 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; @@ -1466,8 +1481,8 @@ static void job_callback(void *obj, struct kevent *kev) 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; @@ -1624,14 +1639,25 @@ static void job_start_child(struct jobcb *j, int execfd) 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; @@ -1640,12 +1666,17 @@ static void job_start_child(struct jobcb *j, int execfd) 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) @@ -1714,11 +1745,11 @@ 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))) { @@ -1729,27 +1760,27 @@ static void job_setup_attributes(struct jobcb *j) 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))) @@ -2001,13 +2032,13 @@ static void reload_launchd_config(void) 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]); @@ -2256,20 +2287,20 @@ static void job_set_alarm(struct jobcb *j) 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) @@ -2287,9 +2318,9 @@ static void job_set_alarm(struct jobcb *j) 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++; @@ -2297,13 +2328,13 @@ static void job_set_alarm(struct jobcb *j) 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++; @@ -2312,7 +2343,7 @@ static void job_set_alarm(struct jobcb *j) 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; diff --git a/launchd/src/launchd.plist.5 b/launchd/src/launchd.plist.5 index 96844de..e9ab97a 100644 --- a/launchd/src/launchd.plist.5 +++ b/launchd/src/launchd.plist.5 @@ -91,6 +91,7 @@ This optional key specifies the user to run the job as. The default is the group .It Sy inetdCompatibility 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 This flag corresponds to the "wait" or "nowait" option of @@ -126,7 +127,7 @@ before running the job. .It Sy ServiceIPC 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 The recommended time out to pass to the job. If no value is specified, a default time out will be supplied by .Nm launchd @@ -134,7 +135,7 @@ for use by the job at check in time. .It Sy InitGroups 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 This optional key causes the job to be started if any one of the listed paths are modified. @@ -142,10 +143,16 @@ This optional key causes the job to be started if any one of the listed paths ar 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 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 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 The minute on which this job will be run. @@ -168,7 +175,6 @@ This optional key specifies what file should be used for data being sent to stde 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 .It Sy HardResourceLimits Resource limits to be imposed on the job. These adjust variables set with @@ -209,11 +215,17 @@ This optional key specifies what value should be applied to the daemon. .It Sy LowPriorityIO This optional key specifies whether the kernel should consider this daemon to be low priority when doing file system I/O. -.It Sy Sockets +.It Sy Sockets 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 @@ -256,6 +268,12 @@ to. 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 +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 diff --git a/launchd/testing/StartCalendarInterval.plist b/launchd/testing/StartCalendarInterval.plist new file mode 100644 index 0000000..503a4b3 --- /dev/null +++ b/launchd/testing/StartCalendarInterval.plist @@ -0,0 +1,16 @@ + + + + + Label + com.apple.launchd.test.StartCalendarInterval + ProgramArguments + + echo + StartCalendarInterval + + StartCalendarInterval + + + + -- 2.45.2