1/*
2 * CDDL HEADER START
3 *
4 * The contents of this file are subject to the terms of the
5 * Common Development and Distribution License (the "License").
6 * You may not use this file except in compliance with the License.
7 *
8 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9 * or http://www.opensolaris.org/os/licensing.
10 * See the License for the specific language governing permissions
11 * and limitations under the License.
12 *
13 * When distributing Covered Code, include this CDDL HEADER in each
14 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15 * If applicable, add the following below this CDDL HEADER, with the
16 * fields enclosed by brackets "[]" replaced with your own identifying
17 * information: Portions Copyright [yyyy] [name of copyright owner]
18 *
19 * CDDL HEADER END
20 */
21/*
22 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
23 * Use is subject to license terms.
24 */
25
26#include <time.h>
27#include <stdio.h>
28#include <assert.h>
29#include <string.h>
30#include <stdlib.h>
31#include <unistd.h>
32#include <sys/types.h>
33#include <sys/wait.h>
34#include <signal.h>
35#include <fcntl.h>
36#include <dhcpmsg.h>
37
38#include "agent.h"
39#include "script_handler.h"
40#include "states.h"
41#include "interface.h"
42
43/*
44 * scripts are directly managed by a script helper process. dhcpagent creates
45 * the helper process and it, in turn, creates a process to run the script
46 * dhcpagent owns one end of a pipe and the helper process owns the other end
47 * the helper process calls waitpid to wait for the script to exit. an alarm
48 * is set for SCRIPT_TIMEOUT seconds. If the alarm fires, SIGTERM is sent to
49 * the script process and a second alarm is set for SCRIPT_TIMEOUT_GRACE. if
50 * the second alarm fires, SIGKILL is sent to forcefully kill the script. when
51 * script exits, the helper process notifies dhcpagent by closing its end
52 * of the pipe.
53 */
54
55unsigned int	script_count;
56
57/*
58 * the signal to send to the script process. it is a global variable
59 * to this file as sigterm_handler needs it.
60 */
61
62static int	script_signal = SIGTERM;
63
64/*
65 * script's absolute timeout value. the first timeout is set to SCRIPT_TIMEOUT
66 * seconds from the time it is started. SIGTERM is sent on the first timeout
67 * the second timeout is set to SCRIPT_TIMEOUT_GRACE from the first timeout
68 * and SIGKILL is sent on the second timeout.
69 */
70static time_t	timeout;
71
72/*
73 * sigalarm_handler(): signal handler for SIGALRM
74 *
75 *   input: int: signal the handler was called with
76 *  output: void
77 */
78
79/* ARGSUSED */
80static void
81sigalarm_handler(int sig)
82{
83	time_t	now;
84
85	/* set a another alarm if it fires too early */
86	now = time(NULL);
87	if (now < timeout)
88		(void) alarm(timeout - now);
89}
90
91/*
92 * sigterm_handler(): signal handler for SIGTERM, fired when dhcpagent wants
93 *		      to stop the script
94 *   input: int: signal the handler was called with
95 *  output: void
96 */
97
98/* ARGSUSED */
99static void
100sigterm_handler(int sig)
101{
102	if (script_signal != SIGKILL) {
103		/* send SIGKILL SCRIPT_TIMEOUT_GRACE seconds from now */
104		script_signal = SIGKILL;
105		timeout = time(NULL) + SCRIPT_TIMEOUT_GRACE;
106		(void) alarm(SCRIPT_TIMEOUT_GRACE);
107	}
108}
109
110/*
111 * run_script(): it forks a process to execute the script
112 *
113 *   input: dhcp_smach_t *: the state machine
114 *	    const char *: the event name
115 *	    int: the pipe end owned by the script helper process
116 *  output: void
117 */
118
119static void
120run_script(dhcp_smach_t *dsmp, const char *event, int fd)
121{
122	int		n;
123	char		c;
124	char		*path;
125	char		*name;
126	pid_t		pid;
127	time_t		now;
128
129	if ((pid = fork()) == -1)
130		return;
131
132	if (pid == 0) {
133		path = SCRIPT_PATH;
134		name = strrchr(path, '/') + 1;
135
136		/* close all files */
137		closefrom(0);
138
139		/* redirect stdin, stdout and stderr to /dev/null */
140		if ((n = open("/dev/null", O_RDWR)) < 0)
141			_exit(127);
142
143		(void) dup2(n, STDOUT_FILENO);
144		(void) dup2(n, STDERR_FILENO);
145		(void) execl(path, name, dsmp->dsm_name, event, NULL);
146		_exit(127);
147	}
148
149	/*
150	 * the first timeout fires SCRIPT_TIMEOUT seconds from now.
151	 */
152	timeout = time(NULL) + SCRIPT_TIMEOUT;
153	(void) sigset(SIGALRM, sigalarm_handler);
154	(void) alarm(SCRIPT_TIMEOUT);
155
156	/*
157	 * pass script's pid to dhcpagent.
158	 */
159	(void) write(fd, &pid, sizeof (pid));
160
161	for (;;) {
162		if (waitpid(pid, NULL, 0) >= 0) {
163			/* script has exited */
164			c = SCRIPT_OK;
165			break;
166		}
167
168		if (errno != EINTR)
169			return;
170
171		now = time(NULL);
172		if (now >= timeout) {
173			(void) kill(pid, script_signal);
174			if (script_signal == SIGKILL) {
175				c = SCRIPT_KILLED;
176				break;
177			}
178
179			script_signal = SIGKILL;
180			timeout = now + SCRIPT_TIMEOUT_GRACE;
181			(void) alarm(SCRIPT_TIMEOUT_GRACE);
182		}
183	}
184
185	(void) write(fd, &c, 1);
186}
187
188/*
189 * script_init(): initialize script state on a given state machine
190 *
191 *   input: dhcp_smach_t *: the state machine
192 *  output: void
193 */
194
195void
196script_init(dhcp_smach_t *dsmp)
197{
198	dsmp->dsm_script_pid = -1;
199	dsmp->dsm_script_helper_pid = -1;
200	dsmp->dsm_script_event_id = -1;
201	dsmp->dsm_script_fd = -1;
202	dsmp->dsm_script_callback = NULL;
203	dsmp->dsm_script_event = NULL;
204	dsmp->dsm_callback_arg = NULL;
205}
206
207/*
208 * script_cleanup(): cleanup helper function
209 *
210 *   input: dhcp_smach_t *: the state machine
211 *  output: void
212 */
213
214static void
215script_cleanup(dhcp_smach_t *dsmp)
216{
217	/*
218	 * We must clear dsm_script_pid prior to invoking the callback or we
219	 * could get in an infinite loop via async_finish().
220	 */
221	dsmp->dsm_script_pid = -1;
222	dsmp->dsm_script_helper_pid = -1;
223
224	if (dsmp->dsm_script_fd != -1) {
225		assert(dsmp->dsm_script_event_id != -1);
226		(void) iu_unregister_event(eh, dsmp->dsm_script_event_id, NULL);
227		(void) close(dsmp->dsm_script_fd);
228
229		assert(dsmp->dsm_script_callback != NULL);
230		dsmp->dsm_script_callback(dsmp, dsmp->dsm_callback_arg);
231		script_init(dsmp);
232		script_count--;
233		release_smach(dsmp);	/* hold from script_start() */
234	}
235}
236
237/*
238 * script_exit(): does cleanup and invokes the callback when the script exits
239 *
240 *   input: eh_t *: unused
241 *	    int: the end of pipe owned by dhcpagent
242 *	    short: unused
243 *	    eh_event_id_t: unused
244 *	    void *: the state machine
245 *  output: void
246 */
247
248/* ARGSUSED */
249static void
250script_exit(iu_eh_t *ehp, int fd, short events, iu_event_id_t id, void *arg)
251{
252	char c;
253
254	if (read(fd, &c, 1) <= 0)
255		c = SCRIPT_FAILED;
256
257	if (c == SCRIPT_OK)
258		dhcpmsg(MSG_DEBUG, "script ok");
259	else if (c == SCRIPT_KILLED)
260		dhcpmsg(MSG_DEBUG, "script killed");
261	else
262		dhcpmsg(MSG_DEBUG, "script failed");
263
264	script_cleanup(arg);
265}
266
267/*
268 * script_start(): tries to start a script.
269 *		   if a script is already running, it's stopped first.
270 *
271 *
272 *   input: dhcp_smach_t *: the state machine
273 *	    const char *: the event name
274 *	    script_callback_t: callback function
275 *	    void *: data to the callback function
276 *  output: boolean_t: B_TRUE if script starts successfully
277 *	    int *: the returned value of the callback function if script
278 *		starts unsuccessfully
279 */
280
281boolean_t
282script_start(dhcp_smach_t *dsmp, const char *event,
283    script_callback_t *callback, void *arg, int *status)
284{
285	int		n;
286	int		fds[2];
287	pid_t		pid;
288	iu_event_id_t	event_id;
289
290	assert(callback != NULL);
291
292	if (dsmp->dsm_script_pid != -1) {
293		/* script is running, stop it */
294		dhcpmsg(MSG_DEBUG, "script_start: stopping ongoing script");
295		script_stop(dsmp);
296	}
297
298	if (access(SCRIPT_PATH, X_OK) == -1) {
299		/* script does not exist */
300		goto out;
301	}
302
303	/*
304	 * dhcpagent owns one end of the pipe and script helper process
305	 * owns the other end. dhcpagent reads on the pipe; and the helper
306	 * process notifies it when the script exits.
307	 */
308	if (pipe(fds) < 0) {
309		dhcpmsg(MSG_ERROR, "script_start: can't create pipe");
310		goto out;
311	}
312
313	if ((pid = fork()) < 0) {
314		dhcpmsg(MSG_ERROR, "script_start: can't fork");
315		(void) close(fds[0]);
316		(void) close(fds[1]);
317		goto out;
318	}
319
320	if (pid == 0) {
321		/*
322		 * SIGCHLD is ignored in dhcpagent, the helper process
323		 * needs it. it calls waitpid to wait for the script to exit.
324		 */
325		(void) close(fds[0]);
326		(void) sigset(SIGCHLD, SIG_DFL);
327		(void) sigset(SIGTERM, sigterm_handler);
328		run_script(dsmp, event, fds[1]);
329		exit(0);
330	}
331
332	(void) close(fds[1]);
333
334	/* get the script's pid */
335	if (read(fds[0], &dsmp->dsm_script_pid, sizeof (pid_t)) !=
336	    sizeof (pid_t)) {
337		(void) kill(pid, SIGKILL);
338		dsmp->dsm_script_pid = -1;
339		(void) close(fds[0]);
340		goto out;
341	}
342
343	dsmp->dsm_script_helper_pid = pid;
344	event_id = iu_register_event(eh, fds[0], POLLIN, script_exit, dsmp);
345	if (event_id == -1) {
346		(void) close(fds[0]);
347		script_stop(dsmp);
348		goto out;
349	}
350
351	script_count++;
352	dsmp->dsm_script_event_id = event_id;
353	dsmp->dsm_script_callback = callback;
354	dsmp->dsm_script_event = event;
355	dsmp->dsm_callback_arg = arg;
356	dsmp->dsm_script_fd = fds[0];
357	hold_smach(dsmp);
358	return (B_TRUE);
359
360out:
361	/* callback won't be called in script_exit, so call it here */
362	n = callback(dsmp, arg);
363	if (status != NULL)
364		*status = n;
365
366	return (B_FALSE);
367}
368
369/*
370 * script_stop(): stops the script if it is running
371 *
372 *   input: dhcp_smach_t *: the state machine
373 *  output: void
374 */
375
376void
377script_stop(dhcp_smach_t *dsmp)
378{
379	if (dsmp->dsm_script_pid != -1) {
380		assert(dsmp->dsm_script_helper_pid != -1);
381
382		/*
383		 * sends SIGTERM to the script and asks the helper process
384		 * to send SIGKILL if it does not exit after
385		 * SCRIPT_TIMEOUT_GRACE seconds.
386		 */
387		(void) kill(dsmp->dsm_script_pid, SIGTERM);
388		(void) kill(dsmp->dsm_script_helper_pid, SIGTERM);
389	}
390
391	script_cleanup(dsmp);
392}
393