1 /*
2 * sqsh_job.c - Functions for launching commands
3 *
4 * Copyright (C) 1995, 1996 by Scott C. Gray
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, write to the Free Software
18 * Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
19 *
20 * You may contact the author :
21 * e-mail: gray@voicenet.com
22 * grays@xtend-tech.com
23 * gray@xenotropic.com
24 */
25 #include <stdio.h>
26 #include <sys/param.h>
27 #include <ctype.h>
28 #include <setjmp.h>
29 #include "sqsh_config.h"
30 #include "sqsh_error.h"
31 #include "sqsh_fd.h"
32 #include "sqsh_init.h"
33 #include "sqsh_tok.h"
34 #include "sqsh_cmd.h"
35 #include "sqsh_global.h"
36 #include "sqsh_fork.h"
37 #include "sqsh_expand.h"
38 #include "sqsh_strchr.h"
39 #include "sqsh_alias.h"
40 #include "sqsh_getopt.h"
41 #include "sqsh_sig.h"
42 #include "sqsh_job.h"
43
44 /*-- Current Version --*/
45 #if !defined(lint) && !defined(__LINT__)
46 static char RCS_Id[] = "$Id: sqsh_job.c,v 1.11 2014/03/14 17:24:53 mwesdorp Exp $";
47 USE(RCS_Id)
48 #endif /* !defined(lint) */
49
50 /*-- Local Prototypes --*/
51 static job_t* job_alloc _ANSI_ARGS((jobset_t*));
52 static int job_free _ANSI_ARGS((job_t*));
53 static job_id_t jobset_wait_all _ANSI_ARGS((jobset_t*, int*, int));
54 static int jobset_parse _ANSI_ARGS((jobset_t*, job_t*, char*,
55 varbuf_t*, int));
56 static job_t* jobset_get _ANSI_ARGS((jobset_t*, job_id_t));
57 static void jobset_run_sigint _ANSI_ARGS((int, void*));
58 static int jobset_get_cmd _ANSI_ARGS((jobset_t*, char*, cmd_t**));
59 static int jobset_global_init _ANSI_ARGS((void));
60
61 /*-- Status Globals --*/
62 static JMP_BUF sg_jmp_buf ; /* Where to go on SIGINT */
63
64 /*
65 * sg_cmd_buf: Variable length buffer to be used to expand the
66 * current command to be executed.
67 */
68 static varbuf_t *sg_cmd_buf = NULL ; /* Expanded command buffer */
69
70 /*
71 * sg_alias_buf: Variable length buffer to be used when performing
72 * alias expansion (prior to command expansion).
73 */
74 static varbuf_t *sg_alias_buf = NULL ; /* Expanded alias buffer */
75
76 /*
77 * sg_while_buf: Variable length buffer used to hold the (mostly)
78 * unparsed command line for a \while statement.
79 */
80 static varbuf_t *sg_while_buf = NULL ; /* Expanded alias buffer */
81
82 /*
83 * jobset_create():
84 *
85 * Creates and attaches a new jobset structure to the current context.
86 * Upon success, True is return, otherwise False is returned.
87 */
jobset_create(hsize)88 jobset_t* jobset_create( hsize )
89 int hsize;
90 {
91 jobset_t *js;
92 int i;
93
94 /*-- Always check your parameters --*/
95 if( hsize < 1 ) {
96 sqsh_set_error( SQSH_E_BADPARAM, NULL );
97 return False;
98 }
99
100 /*-- Attempt to allocate a new jobset structure --*/
101 if( (js = (jobset_t*)malloc( sizeof( jobset_t ) )) == NULL ) {
102 sqsh_set_error( SQSH_E_NOMEM, NULL );
103 return NULL;
104 }
105
106 /*-- Allocate the hash table for the job_id's --*/
107 if( (js->js_jobs = (job_t**)malloc(sizeof(job_t*)*hsize)) == NULL ){
108 free( js );
109 sqsh_set_error( SQSH_E_NOMEM, NULL );
110 return NULL;
111 }
112
113 /*
114 * Allocate a sigcld_t. This will act as a handle on all of the
115 * SIGCHLD events that will be received due to background jobs
116 * completing.
117 * sqsh-2.1.7 - Logical fix: Free js->js_jobs before a free of js.
118 */
119 if( (js->js_sigcld = sigcld_create()) == NULL ) {
120 free( js->js_jobs );
121 free( js );
122 sqsh_set_error( sqsh_get_error(), "sigcld_create: %s", sqsh_get_errstr() );
123 return NULL;
124 }
125
126 /*-- Initialize our command structure --*/
127 js->js_hsize = hsize;
128
129 for( i = 0; i < hsize; i++ )
130 js->js_jobs[i] = NULL;
131
132 sqsh_set_error( SQSH_E_NONE, NULL );
133 return js;
134 }
135
136
137 /*
138 * jobset_is_cmd():
139 *
140 * Returns a positive value of cmd_line is a command string, 0 if it
141 * does not, and a negative value upon error.
142 */
jobset_is_cmd(js,cmd_line)143 int jobset_is_cmd( js, cmd_line )
144 jobset_t *js;
145 char *cmd_line;
146 {
147 cmd_t *cmd;
148
149 sqsh_set_error( SQSH_E_NONE, NULL );
150
151 /*-- Alias' are assumed to be commands --*/
152 if( alias_test( g_alias, cmd_line ) > 0 )
153 return 1;
154 return jobset_get_cmd( js, cmd_line, &cmd );
155 }
156
157 /*
158 * jobset_get_cmd():
159 *
160 */
jobset_get_cmd(js,cmd_line,cmd)161 static int jobset_get_cmd( js, cmd_line, cmd )
162 jobset_t *js;
163 char *cmd_line;
164 cmd_t **cmd;
165 {
166 tok_t *tok ; /* Token returned from sqsh_tok() */
167
168 /*
169 * The jobset_global_init() initializes the sg_cmd_buf global
170 * variable which we will need to expand the command name
171 * into.
172 */
173 if( jobset_global_init() == False )
174 return -1;
175
176 /*
177 * Now we want to expand just the first syntactical word of
178 * the command line into a temporary buffer. If there is an
179 * error then we just assume that it is bad quoting or something
180 * like that and therefore this isn't a valid command line.
181 */
182 if( sqsh_nexpand( cmd_line, sg_cmd_buf, 0, EXP_WORD ) == False )
183 return 0;
184
185 DBG(sqsh_debug( DEBUG_JOB, "jobset_get_cmd: Testing '%s'\n",
186 varbuf_getstr(sg_cmd_buf) );)
187
188 /*
189 * Try to grab the first token from the command line. If there
190 * is any error while tokenizing, then we will assume that this
191 * isn't a command.
192 */
193 if( sqsh_tok( varbuf_getstr( sg_cmd_buf ), &tok, 0 ) == False )
194 return 0;
195
196 /*
197 * So, if the first token we retrieve isn't a word (i.e. it is a
198 * pipe, or a redirection or something), or we couldn't find the
199 * command in our global set of commands, then we don't have a
200 * command.
201 */
202 if( tok->tok_type != SQSH_T_WORD ||
203 (*cmd = cmdset_get(g_cmdset, sqsh_tok_value(tok))) == NULL )
204 return 0;
205
206 return 1;
207 }
208
209 /*
210 * jobset_run():
211 *
212 * Attempts to run the command contained in cmd_line. If the job is
213 * succesfully run and completes (i.e. wasn't a background job), then
214 * 0 is returned and exit_status will contained the exit status of the
215 * command. If the job was succesfully started and is still running then
216 * a valid job_id_t is returned as a handle to the running job.
217 * If there was any sort of error then -1 is returned.
218 */
jobset_run(js,cmd_line,exit_status)219 job_id_t jobset_run( js, cmd_line, exit_status )
220 jobset_t *js;
221 char *cmd_line;
222 int *exit_status;
223 {
224 cmd_t *cmd ; /* The command being run */
225 char msg[512];
226 int error;
227 pid_t child_pid;
228 int ret;
229 int hval;
230 job_t *job;
231 int tok_flags;
232
233 #if defined(USE_AIX_FIX)
234 int old_flags = (int)-1;
235 #endif /* USE_AIX_FIX */
236 varbuf_t *while_buf;
237
238 /*-- Check the arguments --*/
239 if( js == NULL || cmd_line == NULL ) {
240 sqsh_set_error( SQSH_E_BADPARAM, NULL );
241 return -1;
242 }
243
244 /*
245 * We need to initialize a couple of global buffers that
246 * we are going to use, namely sg_cmd_buf.
247 */
248 if( jobset_global_init() == False )
249 return -1;
250
251 /*
252 * Perform alias expansion.
253 */
254 if( alias_expand( g_alias, cmd_line, sg_alias_buf ) > 0 )
255 cmd_line = varbuf_getstr( sg_alias_buf );
256
257 /*
258 * Attempt to retrieve the command from the command line. If it
259 * wasn't a command, then return an error condition. jobset_get_cmd
260 * should take care of setting the appropriate error message for
261 * us.
262 */
263 if( (ret = jobset_get_cmd( js, cmd_line, &cmd )) <= 0 ) {
264 if( ret == 0 )
265 sqsh_set_error( SQSH_E_EXIST, "Invalid command" );
266 return -1;
267 }
268
269 /*
270 * Token flags control how the tokenizer behaves.
271 */
272 tok_flags = 0;
273
274 /*
275 * When parsing the \if expression, expand '[' and ']' to be
276 * a test statement.
277 */
278 if (strcmp( cmd->cmd_name, "\\if" ) == 0)
279 {
280 tok_flags |= TOK_F_TEST;
281 }
282
283 /*
284 * \while is a special case. We need to make sure that
285 * we do *not* expand the command line of variables and
286 * that the command line is passed in as a single string.
287 */
288 if (strcmp( cmd->cmd_name, "\\while" ) == 0)
289 {
290 /*
291 * Leave all quotes and other goodies in-tact.
292 */
293 tok_flags |= TOK_F_LEAVEQUOTE|TOK_F_TEST;
294
295 /*
296 * We'll pass this varbuf_t into the tokenizer to fill with
297 * the unparsed command line.
298 */
299 while_buf = sg_while_buf;
300 varbuf_clear( while_buf );
301 }
302 else
303 {
304 /*
305 * Let the parser know that this isn't a \while statement.
306 */
307 while_buf = NULL;
308
309 /*
310 * Expand the command line of any tokens, keeping quotes and stripping
311 * escape characters (i.e. removing them as part of the expansion
312 * process).
313 * sqsh-2.5 : Implemented tilde expansion in sqsh_expand().
314 */
315 if (sqsh_expand( cmd_line, sg_cmd_buf, EXP_COLUMNS | EXP_TILDE ) == False)
316 {
317 sqsh_set_error( sqsh_get_error(), "sqsh_expand: %s", sqsh_get_errstr() );
318 return -1;
319 }
320
321 /*
322 * The "actual" command line is the version that we just expanded
323 * of any variables.
324 */
325 cmd_line = varbuf_getstr( sg_cmd_buf );
326 }
327
328 /*
329 * It was a command, so lets allocate a new job structure for
330 * the duration of the command.
331 */
332 if ((job = job_alloc(js)) == NULL)
333 {
334 sqsh_set_error( SQSH_E_NOMEM, NULL );
335 return -1;
336 }
337
338 /*
339 * Well, it looks like we are in for the long hual. The first
340 * thing we need to do at this point is back up the state of all
341 * of our file descriptors. This allows jobset_parse() to do all
342 * of the opening and closing of file descriptors that it could
343 * possible want to and a single call to sqsh_frestore() will
344 * restore the current state.
345 */
346 if (sqsh_fsave() == -1)
347 {
348 sqsh_set_error( sqsh_get_error(), "sqsh_fsave: %s", sqsh_get_errstr() );
349 job_free( job );
350 return -1;
351 }
352
353 #if defined(USE_AIX_FIX)
354 old_flags = stdout->_flag;
355 #endif /* USE_AIX_FIX */
356
357 /*
358 * Now, let jobset_parse() do its thing and redirect stdout
359 * and the such as necessary. If it has an error then we just send
360 * it back with an error code.
361 */
362 if (jobset_parse( js, job, cmd_line, while_buf, tok_flags ) == False)
363 {
364 goto jobset_abort;
365 }
366
367 /*
368 * If this was a \while statement, then the sole argument to the
369 * function will be the unexpanded command line stripped of
370 * all redirection crap.
371 */
372 if (while_buf != NULL)
373 {
374 if (args_add(job->job_args, "\\while" ) == False ||
375 args_add(job->job_args, varbuf_getstr(while_buf)) == False)
376 {
377 sqsh_set_error( sqsh_get_error(), "args_add: %s", sqsh_get_errstr() );
378 goto jobset_abort;
379 }
380 }
381
382 /*
383 * It parsed OK, so now job->job_args contains the arguments to
384 * the command and stdout and stderr are redirected as needed.
385 * the only thing left is to fork if necessary and call the
386 * appropriate function.
387 */
388 if (job->job_flags & JOB_BG)
389 {
390 /*
391 * Perform the actual fork(). Sqsh_fork() will take care of
392 * resetting certain global variables in the context of the
393 * child.
394 */
395 switch ((child_pid = sqsh_fork()))
396 {
397 case -1 : /* Error */
398 goto jobset_abort;
399
400 /*
401 * Child process. There really isn't much to do here other
402 * than call the requested cmd and exit with the return value
403 * of that command. The parent will receive the exit status.
404 */
405 case 0 : /* Child process */
406 /*
407 * We want the child to process signals a little differently
408 * from the parent. There is no real need for graceful re-
409 * covery within the child.
410 * sqsh-2.1.7 - Ignore SIGINT (Ctrl-C) interrupts from parent.
411 * Let the child perform a proper sqsh_exit().
412 * Return code is not very relevant here.
413 */
414 while (sig_restore() >= 0);
415 sig_install ( SIGINT, SIG_H_IGN, NULL, 0 );
416 sqsh_getopt_reset();
417 ret = cmd->cmd_ptr( args_argc(job->job_args),
418 args_argv(job->job_args) );
419 sqsh_exit(0);
420
421 /*
422 * Parent process. All we need to do is record the pid of the
423 * child in the job structure, restore the file descriptors
424 * and return.
425 * sqsh-2.1.7 - Make sure SIGCHLD signals are unblocked.
426 */
427 default : /* Parent */
428
429 job->job_pid = child_pid;
430 sigcld_watch( js->js_sigcld, job->job_pid );
431 sigcld_unblock () ;
432 }
433
434 /*
435 * Restore the file descriptors redirected during jobset_parse()
436 * I should probably do something if this fails, but I can't think
437 * of any way to recover.
438 */
439 sqsh_frestore();
440
441 /*
442 * The following is required on those systems in which EOF isn't
443 * checked each time a read operation is attempted on a FILE
444 * structure. This is in case stdin was temporarily redirected
445 * from a file that has reached EOF.
446 */
447 clearerr( stdin );
448
449 /*
450 * Link the new job into our hash table.
451 */
452 hval = job->job_id % js->js_hsize;
453 job->job_nxt = js->js_jobs[hval];
454 js->js_jobs[hval] = job;
455
456 /*-- And return it --*/
457 return job->job_id;
458 }
459
460 /*
461 * This isn't a background job, so we just need to call the command
462 * and return its exit status. We need to install a SIGINT handle
463 * to capture any ^C's from a user. In this case we want the
464 * ^C to return immediately before the sqsh_restore() call below.
465 */
466 sig_save();
467
468 sig_install( SIGINT, jobset_run_sigint, (void*)NULL, 0 );
469 if (SETJMP( sg_jmp_buf ) == 0)
470 {
471 /*
472 * Since most commands use sqsh_getopt() to parse the command
473 * line options, we want to ensure that sqsh_getopt() will
474 * know that it is receiving a new argv[] and argc so we want
475 * to reset its existing impression of these two arguments.
476 */
477 sqsh_getopt_reset();
478
479 /*
480 * If this is a pipe-line then we want to ignore the SIGPIPE. Let the
481 * command itself set up a handler for it if it wishes.
482 */
483 if (job->job_flags & JOB_PIPE)
484 {
485
486 sig_install( SIGPIPE, SIG_H_POLL, (void*)NULL, 0 );
487 *exit_status = cmd->cmd_ptr( args_argc(job->job_args), args_argv(job->job_args) );
488
489 }
490 else
491 {
492 *exit_status = cmd->cmd_ptr( args_argc(job->job_args), args_argv(job->job_args) );
493 }
494 }
495
496 /*-- Restore signal handlers --*/
497 sig_restore();
498
499 /*
500 * Restore the file descriptors that were redirected during the
501 * process of parsing the command line.
502 */
503 sqsh_frestore();
504
505 /*
506 * As with before, the following is required just in case stdin
507 * hit EOF while redirected from a file or some other file
508 * descriptor. On some systems EOF sticks with the FILE structure
509 * until clearerr() is called.
510 */
511 clearerr( stdin );
512
513 #if defined(USE_AIX_FIX)
514 stdout->_flag = old_flags;
515 #endif /* USE_AIX_FIX */
516
517 /*
518 * Lastly, destroy the job structure that we allocated. It almost
519 * makes you wonder why it was created at all.
520 */
521 job_free( job );
522
523 /*
524 * Note, reguardless of the exit status of the command, we return 0
525 * (i.e. the command was called succesfully reguarldess of whether
526 * or not the job performed its task.
527 */
528 sqsh_set_error( SQSH_E_NONE, NULL );
529 return 0;
530
531 jobset_abort:
532 /*
533 * This sucks. Unfortunately I want to return the error condition
534 * returned by jobset_parse(), but the functions sqsh_frestore() and
535 * job_free() probably set the sqsh error condition, so we need to
536 * save the current error condition prior to calling them.
537 * There must be a better way of doing this.
538 */
539 strcpy( msg, sqsh_get_errstr() );
540 error = sqsh_get_error();
541 sqsh_frestore();
542 clearerr( stdin );
543 #if defined(USE_AIX_FIX)
544 if (old_flags != (int)-1)
545 {
546 stdout->_flag = old_flags;
547 }
548 #endif /* USE_AIX_FIX */
549 job_free( job );
550 sqsh_set_error( error, msg );
551 return -1;
552 }
553
554 /*
555 * jobset_run_sigint():
556 *
557 * This little stub is used by jobset_run() to capture SIGINT from a
558 * user and return to the point immedately after the command was executed..
559 */
jobset_run_sigint(sig,user_data)560 static void jobset_run_sigint( sig, user_data )
561 int sig;
562 void *user_data;
563 {
564 LONGJMP( sg_jmp_buf, 1 );
565 }
566
567 /*
568 * jobset_wait():
569 *
570 * Waits for job given by job_id to complete execution (if job_id is
571 * a negative value then jobset_wait() waits for any pending job to
572 * complete). If block_type is JOBSET_NONBLOCK and the job has not
573 * completed then 0 is returned and exit_status contains an undefined
574 * value, otherwise, if block_type is JOBSET_BLOCK, the job_id of
575 * the completed job is returned with exit_status containing the exit
576 * value of the job. If an error condition ocurres, or job_id is not a
577 * valid pending job then -1 is returned.
578 */
jobset_wait(js,job_id,exit_status,block_type)579 job_id_t jobset_wait( js, job_id, exit_status, block_type )
580 jobset_t *js;
581 job_id_t job_id;
582 int *exit_status;
583 int block_type;
584 {
585 job_t *j;
586 int wait_type;
587 pid_t pid;
588
589 /*-- Always check your parameters --*/
590 if( js == NULL || exit_status == NULL || (block_type != JOB_BLOCK && block_type != JOB_NONBLOCK) ) {
591 sqsh_set_error( SQSH_E_BADPARAM, NULL );
592 return -1;
593 }
594
595 if( job_id < 0 )
596 return jobset_wait_all( js, exit_status, block_type );
597
598 /*-- Find out if the job exists --*/
599 /* (sqsh-2.1.7 - Bug fix for bucket calculation) --*/
600 for( j = js->js_jobs[job_id % js->js_hsize];
601 j != NULL && j->job_id != job_id ; j = j->job_nxt );
602
603 /*-- If we can't, error --*/
604 if( j == NULL ) {
605 sqsh_set_error( SQSH_E_EXIST, NULL );
606 return -1;
607 }
608
609 /*
610 * If the job has already been recorded as being complete then
611 * just return the necessary information to the caller.
612 */
613 if( j->job_flags & JOB_DONE ) {
614 *exit_status = j->job_status;
615 return j->job_id;
616 }
617
618 /*
619 * The type of sigcld wait that will be performed depends on
620 * what kind of blocking is requested by the caller.
621 */
622 wait_type = (block_type == JOB_BLOCK) ? SIGCLD_BLOCK : SIGCLD_NONBLOCK;
623
624 /*
625 * Go ahead and wait for the completion of the job.
626 */
627 pid = sigcld_wait( js->js_sigcld, j->job_pid, exit_status, wait_type );
628
629 /*
630 * If block_type was JOB_NONBLOCK and there were no jobs pending,
631 * then return 0.
632 */
633 if( pid == 0 ) {
634 sqsh_set_error( SQSH_E_NONE, NULL );
635 return 0;
636 }
637
638 /*
639 * If we have reached this point, then the job has completed so
640 * it is just a matter of recording the exit status and returning.
641 */
642 j->job_flags |= JOB_DONE;
643 j->job_end = time(NULL);
644 j->job_status = *exit_status;
645
646 /*
647 * Propagate any errors back to the caller.
648 * sqsh-2.1.7 - If we missed a SIGCLD signal and the pid is already
649 * finished, then remove the pid from the watch list, report an error
650 * and continue as normal. Then you can use \show to check the deferred
651 * output so far and get the job out of the queue.
652 */
653 if( pid == -1 ) {
654 sigcld_unwatch ( js->js_sigcld, j->job_pid );
655 sqsh_set_error( sqsh_get_error(), "sigcld_wait: %s", sqsh_get_errstr() );
656 }
657 else {
658 sqsh_set_error( SQSH_E_NONE, NULL );
659 }
660
661 return j->job_id;
662 }
663
jobset_end(js,job_id)664 int jobset_end( js, job_id )
665 jobset_t *js;
666 job_id_t job_id;
667 {
668 job_t *j, *j_prv;
669 int hval;
670 int exit_status;
671
672 /*-- Always check your parameters --*/
673 if( js == NULL || job_id <= 0 ) {
674 sqsh_set_error( SQSH_E_BADPARAM, NULL );
675 return False;
676 }
677
678 /*-- Calculate which bucket job_id is in --*/
679 hval = job_id % js->js_hsize;
680
681 /*-- Search for the job --*/
682 j_prv = NULL;
683 for( j = js->js_jobs[hval]; j != NULL && j->job_id != job_id;
684 j = j->job_nxt )
685 j_prv = j;
686
687 if( j == NULL ) {
688 sqsh_set_error( SQSH_E_EXIST, "Invalid job_id %d", job_id );
689 return False;
690 }
691
692 /*
693 * If the job hasn't completed yet, then we need to kill the process
694 * associated with the job and wait for it to complete.
695 */
696 if( !(j->job_flags & JOB_DONE) ) {
697
698 /*-- Kill the job --*/
699 if( kill( j->job_pid, SIGTERM ) == -1 ) {
700 sqsh_set_error( errno, "Unable to SIGTERM pid %d: %s", j->job_pid, strerror( errno ) );
701 return False;
702 }
703
704 /*-- Wait for it to die. --*/
705 if( sigcld_wait( js->js_sigcld, j->job_pid, &exit_status, SIGCLD_BLOCK ) == -1 ) {
706 sqsh_set_error( sqsh_get_error(), "sigcld_wait: %s", sqsh_get_errstr() );
707 return False;
708 }
709 }
710
711 /*
712 * Finally unlink it from the list of jobs that are running.
713 */
714 if( j_prv != NULL )
715 j_prv->job_nxt = j->job_nxt;
716 else
717 js->js_jobs[hval] = j->job_nxt;
718
719 job_free( j );
720 sqsh_set_error( SQSH_E_NONE, NULL );
721 return True;
722 }
723
724
725 /*
726 * jobset_clear():
727 *
728 * Clears out all references to currently running jobs without
729 * actually terminating the jobs.
730 */
jobset_clear(js)731 int jobset_clear( js )
732 jobset_t *js;
733 {
734 job_t *j, *j_nxt;
735 int i;
736
737 /*-- Check your parameters --*/
738 if( js == NULL ) {
739 sqsh_set_error( SQSH_E_BADPARAM, NULL );
740 return False;
741 }
742
743 /*-- Blast through every bucket in hash table --*/
744 for( i = 0; i < js->js_hsize; i++ ) {
745
746 /*-- Blast through every job in bucket --*/
747 j = js->js_jobs[i];
748 while( j != NULL ) {
749
750 /*-- Keep pointer to next job in bucket --*/
751 j_nxt = j->job_nxt;
752
753 /*-- We no longer care when this job terminates --*/
754 sigcld_unwatch( js->js_sigcld, j->job_pid );
755
756 /*-- Destroy the job --*/
757 job_free( j );
758
759 /*-- Move on --*/
760 j = j_nxt;
761 }
762 }
763
764 sqsh_set_error( SQSH_E_NONE, NULL );
765 return True;
766 }
767
jobset_is_done(js,job_id)768 int jobset_is_done( js, job_id )
769 jobset_t *js;
770 job_id_t job_id;
771 {
772 job_t *j;
773
774 if( (j = jobset_get( js, job_id )) == NULL )
775 return -1;
776
777 sqsh_set_error( SQSH_E_NONE, NULL );
778
779 if( j->job_flags & JOB_DONE )
780 return 1;
781 return 0;
782 }
783
jobset_get_defer(js,job_id)784 char* jobset_get_defer( js, job_id )
785 jobset_t *js;
786 job_id_t job_id;
787 {
788 job_t *j;
789
790 if( (j = jobset_get( js, job_id )) == NULL )
791 return NULL;
792
793 sqsh_set_error( SQSH_E_NONE, NULL );
794 return j->job_output;
795 }
796
jobset_get_pid(js,job_id)797 pid_t jobset_get_pid( js, job_id )
798 jobset_t *js;
799 job_id_t job_id;
800 {
801 job_t *j;
802
803 if( (j = jobset_get( js, job_id )) == NULL )
804 return -1;
805
806 sqsh_set_error( SQSH_E_NONE, NULL );
807 return j->job_pid;
808 }
809
jobset_get_status(js,job_id)810 int jobset_get_status( js, job_id )
811 jobset_t *js;
812 job_id_t job_id;
813 {
814 job_t *j;
815
816 if( (j = jobset_get( js, job_id )) == NULL )
817 return -1;
818
819 sqsh_set_error( SQSH_E_NONE, NULL );
820 return j->job_status;
821 }
822
823 /*
824 * jobset_destroy():
825 *
826 * Destroys a jobset structure, sending a SIGTERM signal to all
827 * child jobs within it that have not yet completed. Compare and
828 * contrast to jobset_clear(). True is returned upon success, False
829 * upon failure.
830 */
jobset_destroy(js)831 int jobset_destroy( js )
832 jobset_t *js;
833 {
834 int i;
835 job_t *j, *j_nxt;
836 int exit_status;
837
838 /*-- Stupid programmers --*/
839 if( js == NULL ) {
840 sqsh_set_error( SQSH_E_BADPARAM, NULL );
841 return False;
842 }
843
844 for( i = 0; i < js->js_hsize; i++ ) {
845 j = js->js_jobs[i];
846 while( j != NULL ) {
847 j_nxt = j->job_nxt;
848
849 /*
850 * If the job hasn't completed yet, then we need to kill the process
851 * associated with the job and wait for it to complete.
852 */
853 if( !(j->job_flags & JOB_DONE) ) {
854
855 /*
856 * Well, I should probably be paying attention to the return
857 * values of the next two calls, but if they fail I really
858 * don't know what to do about it anyway. Either way the
859 * child process is going down.
860 */
861 kill( j->job_pid, SIGTERM );
862 sigcld_wait( js->js_sigcld, j->job_pid, &exit_status, SIGCLD_BLOCK );
863 }
864
865 job_free( j );
866 j = j_nxt;
867 }
868 }
869
870 free( js->js_jobs );
871
872 /*-- Destroy sigcld context structure --*/
873 sigcld_destroy( js->js_sigcld );
874
875 /*-- Finally, free the jobset --*/
876 free( js );
877
878 sqsh_set_error( SQSH_E_NONE, NULL );
879 return True;
880 }
881
882 /*****************************************************************
883 ** INTERNAL FUNTIONS
884 *****************************************************************/
885
886 /*
887 * jobset_wait_all():
888 */
jobset_wait_all(js,exit_status,block_type)889 static job_id_t jobset_wait_all( js, exit_status, block_type )
890 jobset_t *js;
891 int *exit_status;
892 int block_type;
893 {
894 job_t *j;
895 pid_t pid;
896 int wait_type;
897 int i;
898
899 /*
900 * The type of sigcld wait that will be performed depends on
901 * what kind of blocking is requested by the caller.
902 */
903 wait_type = (block_type == JOB_BLOCK) ? SIGCLD_BLOCK : SIGCLD_NONBLOCK;
904
905 /*
906 * Go ahead and wait for the completion of the job.
907 */
908 pid = sigcld_wait( js->js_sigcld, -1, exit_status, wait_type );
909
910 /*
911 * Propagate any errors back to the caller.
912 */
913 if( pid == -1 ) {
914 sqsh_set_error( sqsh_get_error(), "sigcld_wait: %s", sqsh_get_errstr() );
915 return -1;
916 }
917
918 /*
919 * If block_type was JOB_NONBLOCK and there were no jobs pending,
920 * then return 0.
921 */
922 if( pid == 0 ) {
923 sqsh_set_error( SQSH_E_NONE, NULL );
924 return 0;
925 }
926
927 /*
928 * The following two loops rather inneficiently looks up the
929 * job_t structure that represents the completed pid.
930 */
931 for( i = 0; i < js->js_hsize; i++ ) {
932
933 for( j = js->js_jobs[i]; j != NULL && j->job_pid != pid;
934 j = j->job_nxt );
935
936 if( j != NULL ) {
937
938 /*
939 * If we have reached this point, then the job has completed so
940 * it is just a matter of recording the exit status and returning.
941 */
942 j->job_flags |= JOB_DONE;
943 j->job_end = time(NULL);
944 j->job_status = *exit_status;
945
946 sqsh_set_error( SQSH_E_NONE, NULL );
947 return j->job_id;
948 }
949 }
950
951 /*
952 * If we get here then we have trouble...we have received a sigcld
953 * for a pid that we don't have registered as belonging to a job.
954 * Hmmm...what to do?
955 */
956 sqsh_set_error( SQSH_E_EXIST, "Received SIGCHLD for unknown pid!" );
957 return -1;
958 }
959
960 /*
961 * jobset_parse():
962 *
963 * This giant beastie of a function is responsible for parsing the
964 * command line for a job function. While it proceeds it redirects
965 * any file descriptors as requested by the command line. Upon success
966 * various fields of job will be set indicating option and arguments
967 * on the command line and true is returned. Upon failure the status
968 * of all file descriptors and the contents of the job structure
969 * is undefined, False is returned.
970 */
jobset_parse(js,job,cmd_line,while_buf,tok_flags)971 static int jobset_parse( js, job, cmd_line, while_buf, tok_flags )
972 jobset_t *js;
973 job_t *job;
974 char *cmd_line;
975 varbuf_t *while_buf;
976 int tok_flags;
977 {
978 tok_t *tok ; /* Token read with sqsh_tok() */
979 int flag ; /* Flag to be passed to open() */
980 char *defer_bg ; /* Results from env_get() */
981 char *tmp_dir ; /* Results from env_get() */
982 char defer_path[SQSH_MAXPATH+1];
983 int defer_fd;
984 int fd;
985 char *bg_ptr = NULL ; /* Location of & */
986 char *pipe_ptr = NULL ; /* Location of | */
987 char *cp;
988 /* sqsh-2.1.6 - New variables */
989 varbuf_t *exp_buf = NULL ;
990
991
992 /*
993 * In a "real" shell both the | pipeline(s) and the & background
994 * characters take presedence during command line parsing. In
995 * order to simulate this we first must determine if we have either
996 * of these characters in our command line.
997 */
998 pipe_ptr = sqsh_strchr( cmd_line, '|' );
999
1000 /*
1001 * Look for a & that isn't preceded by > (which would be an ocurrance
1002 * of the &> token).
1003 */
1004 cp = cmd_line;
1005 while( (bg_ptr = sqsh_strchr( cp, '&' )) != NULL &&
1006 bg_ptr != cmd_line && *(bg_ptr - 1) == '>' )
1007 cp = bg_ptr + 1;
1008
1009 /*
1010 * Now, verify that the background character is the last character
1011 * on the line.
1012 */
1013 if( bg_ptr != NULL ) {
1014 /*-- Skip trailing white space --*/
1015 for( cp = bg_ptr + 1; *cp != '\0' && isspace((int)*cp); ++cp );
1016
1017 if( *cp != '\0' ) {
1018 sqsh_set_error( SQSH_E_SYNTAX, "& must be last token on line" );
1019 return False;
1020 }
1021 }
1022
1023 /*
1024 * All-righty then. Now, the first thing we need to do is, if we
1025 * are to be run in the background the create the necessary defer
1026 * file and attach it to our stdout and stderr. If, later during
1027 * the parsing the user redirects one of these streams it won't
1028 * really hurt anything.
1029 */
1030 if( bg_ptr != NULL ) {
1031
1032 /*-- Mark this as a background job --*/
1033 job->job_flags |= JOB_BG;
1034
1035 env_get( g_env, "defer_bg", &defer_bg );
1036
1037 if( defer_bg != NULL && *defer_bg != '0' ) {
1038
1039 /*-- Mark this job as having deferred data --*/
1040 job->job_flags |= JOB_DEFER;
1041
1042 /*-- Check to see if the user wants to override --*/
1043 env_get( g_env, "tmp_dir", &tmp_dir );
1044
1045 if( tmp_dir == NULL || *tmp_dir == '\0' )
1046 tmp_dir = SQSH_TMP;
1047 else /* sqsh-2.1.6 feature - Expand tmp_dir variable */
1048 {
1049 if ((exp_buf = varbuf_create( 512 )) == NULL)
1050 {
1051 sqsh_set_error( sqsh_get_error(), "varbuf: %s", sqsh_get_errstr() );
1052 return False;
1053 }
1054 if (sqsh_expand( tmp_dir, exp_buf, 0 ) == False)
1055 tmp_dir = SQSH_TMP;
1056 else
1057 tmp_dir = varbuf_getstr( exp_buf );
1058 }
1059
1060 /*-- Create the defer file --*/
1061 sprintf( defer_path, "%s/sqsh-dfr.%d-%d", tmp_dir, (int) getpid(), job->job_id );
1062 if ( exp_buf != NULL )
1063 varbuf_destroy( exp_buf ); /* sqsh-2.1.6 feature */
1064
1065 /*-- Let the job structure know where it is --*/
1066 if( (job->job_output = sqsh_strdup( defer_path )) == NULL ) {
1067 sqsh_set_error( SQSH_E_NOMEM, NULL );
1068 return False;
1069 }
1070
1071 /*-- Now, open the file --*/
1072 if( (defer_fd = sqsh_open(defer_path,O_CREAT|O_WRONLY|O_TRUNC,0600)) == -1 ) {
1073 sqsh_set_error( sqsh_get_error(), "Unable to open %s: %s", defer_path, sqsh_get_errstr() );
1074 return False;
1075 }
1076
1077 /*
1078 * Now, simply attach the new file descriptor onto
1079 * stdout and stderr.
1080 */
1081 if( sqsh_dup2( defer_fd, fileno(stdout) ) == -1 ||
1082 sqsh_dup2( defer_fd, fileno(stderr) ) == -1 ) {
1083 sqsh_set_error( sqsh_get_error(), "sqsh_dup2: %s", sqsh_get_errstr() );
1084 sqsh_close( defer_fd );
1085 return False;
1086 }
1087
1088 /*-- No longer needed --*/
1089 sqsh_close( defer_fd );
1090 }
1091 }
1092
1093 /*
1094 * Ok, now we need to deal with the pipe-line. If there is one
1095 * to be had, we need to crank the sucker up and attach it to
1096 * our stdout. If this is a background process, the pipeline
1097 * will inherit our deferred stdout and stderr.
1098 */
1099 if( pipe_ptr != NULL ) {
1100
1101 /*
1102 * Temporarily, if the bg_ptr is pointing to the trailing
1103 * '&' character then turn it to a '\0'. We'll restore it
1104 * later. General note: Because we know that cmd_line is
1105 * actually a varbuf_t (behaps a rash assumption), we also
1106 * know that it is alterable.
1107 */
1108 if( bg_ptr != NULL )
1109 *bg_ptr = '\0';
1110
1111 /*-- Mark this as having a pipe-line --*/
1112 job->job_flags |= JOB_PIPE;
1113
1114 /*
1115 * Check to see if this is an empty pipe-line. That is,
1116 * if anything actually follow the | character.
1117 */
1118 for( cp = pipe_ptr + 1; *cp != '\0' && isspace((int)*cp); ++cp );
1119 if( *cp == '\0' ) {
1120 sqsh_set_error( SQSH_E_SYNTAX, "Nothing following |" );
1121 return False;
1122 }
1123
1124 /*
1125 * Now, go ahead and crank up the process that we want to
1126 * communicate with.
1127 */
1128 if( (fd = sqsh_popen( pipe_ptr+1, "w", NULL, NULL )) == -1 )
1129 {
1130 env_set( g_internal_env, "?", "-255" );
1131 sqsh_set_error( sqsh_get_error(), "sqsh_popen: %s", sqsh_get_errstr());
1132 return False;
1133 }
1134
1135 /*
1136 * Now attempt to attach the file descriptor that we just
1137 * created to our stdout.
1138 */
1139 if( sqsh_dup2( fd, fileno(stdout) ) == -1 ) {
1140 sqsh_set_error( sqsh_get_error(), "sqsh_dup2: %s", sqsh_get_errstr() );
1141 sqsh_close( fd );
1142 return False;
1143 }
1144
1145 /*-- Don't need this any more --*/
1146 sqsh_close( fd );
1147
1148 /*
1149 * If necessary, restore the background character to where
1150 * we found it. This is supposed to be a non-destructive
1151 * function call.
1152 */
1153 if( bg_ptr != NULL )
1154 *bg_ptr = '&';
1155 }
1156
1157 /*
1158 * Retrieve the first token from the cmd_line. This really shouldn't
1159 * fail, since we have already done it in jobset_run(), above.
1160 */
1161 if( sqsh_tok( cmd_line, &tok, tok_flags ) == False ) {
1162 sqsh_set_error( sqsh_get_error(), "sqsh_tok: %s", sqsh_get_errstr() );
1163 return False;
1164 }
1165
1166 /*
1167 * We are going to continue looking at tokens until we reach the
1168 * end of the command line. It should be noted that according to
1169 * the logic for sqsh_tok(), the SQSH_T_EOF is considered either
1170 * when we actually reach the end of the line or we hit a |
1171 * or the & (background) character.
1172 */
1173 while( tok->tok_type != SQSH_T_EOF ) {
1174
1175 switch( tok->tok_type ) {
1176
1177 case SQSH_T_REDIR_OUT : /* [n]> file, or [n]>> file */
1178
1179 /*
1180 * Attempt to retrieve the name of the file to redirect
1181 * the file descriptor to.
1182 */
1183 if( sqsh_tok( NULL, &tok, tok_flags ) == False ) {
1184 sqsh_set_error( sqsh_get_error(), "sqsh_tok: %s", sqsh_get_errstr() );
1185 return False;
1186 }
1187
1188 /*
1189 * If the next token we retrieve isn't the name of a file, then
1190 * we have a syntax error.
1191 */
1192 if( tok->tok_type != SQSH_T_WORD ) {
1193 sqsh_set_error( SQSH_E_SYNTAX, "Expected file following >" );
1194 return False;
1195 }
1196
1197 /*
1198 * Figure out which flags we need to supply to open, if the
1199 * file is to be appended to, or created/overwritten.
1200 */
1201 if( tok->tok_append )
1202 flag = O_WRONLY | O_CREAT | O_APPEND;
1203 else
1204 flag = O_WRONLY | O_CREAT | O_TRUNC;
1205
1206 /*-- Now, open the file --*/
1207 cp = sqsh_tok_value(tok);
1208 if( (fd = sqsh_open( cp, flag, 0 )) == -1 ) {
1209 sqsh_set_error( sqsh_get_error(), "Unable to open %s: %s", cp, sqsh_get_errstr() );
1210 return False;
1211 }
1212
1213 /*
1214 * Now, redirect the file descriptor requested by the user
1215 * to the newly opened file and close the file.
1216 */
1217 if( sqsh_dup2( fd, tok->tok_fd_left ) == -1 ) {
1218 sqsh_set_error( sqsh_get_error(), "sqsh_dup2: %s", sqsh_get_errstr() );
1219 return False;
1220 }
1221 sqsh_close( fd );
1222
1223 break;
1224
1225 case SQSH_T_REDIR_IN : /* '< filename' */
1226 /*
1227 * Attempt to retrieve the name of the file to redirect
1228 * in from.
1229 */
1230 if( sqsh_tok( NULL, &tok, tok_flags ) == False ) {
1231 sqsh_set_error( sqsh_get_error(), "sqsh_tok: %s", sqsh_get_errstr() );
1232 return False;
1233 }
1234
1235 /*
1236 * If the next token we retrieve isn't the name of a file, then
1237 * we have a syntax error.
1238 */
1239 if( tok->tok_type != SQSH_T_WORD ) {
1240 sqsh_set_error( SQSH_E_SYNTAX, "Expected file following <" );
1241 return False;
1242 }
1243
1244 /*-- Now, open the file --*/
1245 cp = sqsh_tok_value(tok);
1246 if( (fd = sqsh_open( cp, O_RDONLY, 0 )) == -1 ) {
1247 sqsh_set_error( sqsh_get_error(), "%s: %s", cp, sqsh_get_errstr() );
1248 return False;
1249 }
1250
1251 if( (sqsh_dup2( fd, fileno(stdin) )) == -1 ) {
1252 sqsh_set_error( sqsh_get_error(), "%s: %s", cp, sqsh_get_errstr() );
1253 return False;
1254 }
1255 sqsh_close( fd );
1256 break;
1257
1258 case SQSH_T_REDIR_DUP : /* '[m]>&[n]' */
1259 /*
1260 * This one is easy. Simply call sqsh_dup2() to do the
1261 * dirty work.
1262 */
1263 if( sqsh_dup2( tok->tok_fd_right, tok->tok_fd_left ) == -1 ) {
1264 sqsh_set_error( sqsh_get_error(), "sqsh_dup2: %s", sqsh_get_errstr() );
1265 return False;
1266 }
1267
1268 break;
1269
1270 case SQSH_T_WORD :
1271 /*
1272 * If the token is a generic word on the command line then
1273 * we add it to the list of arguments to be passed to the
1274 * function. For a \while loop, we'll just tack it on to
1275 * the single parameter.
1276 */
1277 if (while_buf != NULL)
1278 {
1279 /*
1280 * If this is the first item on the list, then we
1281 * don't need to stick a space between the previous
1282 * one.
1283 */
1284 if (varbuf_getlen(while_buf) > 0)
1285 {
1286 varbuf_charcat( while_buf, ' ' );
1287 }
1288 varbuf_strcat( while_buf, sqsh_tok_value(tok) );
1289 }
1290 else
1291 {
1292 if (args_add(job->job_args, sqsh_tok_value( tok )) == False)
1293 {
1294 sqsh_set_error( sqsh_get_error(), "args_add: %s", sqsh_get_errstr() );
1295 return False;
1296 }
1297 }
1298 break;
1299
1300 default :
1301 sqsh_set_error( SQSH_E_SYNTAX, "Unknown token %d", tok->tok_type );
1302 return False;
1303 } /* switch */
1304
1305 /*
1306 * Retrieve the next token from the command line.
1307 */
1308 if( sqsh_tok( NULL, &tok, tok_flags ) == False ) {
1309 sqsh_set_error( sqsh_get_error(), "sqsh_tok: %s", sqsh_get_errstr() );
1310 return False;
1311 }
1312
1313 } /* while */
1314
1315 sqsh_set_error( SQSH_E_NONE, NULL );
1316 return True;
1317 }
1318
jobset_get(js,job_id)1319 static job_t* jobset_get( js, job_id )
1320 jobset_t *js;
1321 job_id_t job_id;
1322 {
1323 job_t *j;
1324 int hval;
1325
1326 /*-- Always check your parameters --*/
1327 if( js == NULL ) {
1328 sqsh_set_error( SQSH_E_BADPARAM, NULL );
1329 return NULL;
1330 }
1331
1332 /*-- Calculate which bucket job_id is in --*/
1333 hval = job_id % js->js_hsize;
1334
1335 /*-- Search for the job --*/
1336 for( j = js->js_jobs[hval];
1337 j != NULL && j->job_id != job_id; j = j->job_nxt );
1338
1339 /*-- ESTUPID --*/
1340 if( j == NULL ) {
1341 sqsh_set_error( SQSH_E_EXIST, NULL );
1342 return NULL;
1343 }
1344
1345 return j;
1346 }
1347
1348
1349 /*************************************************************/
1350
1351 /*
1352 * job_alloc():
1353 *
1354 * Internal function to allocate and initialize a new job within
1355 * a job set. Returns a valid pointer on success and NULL upon
1356 * a memory allocation failure.
1357 */
job_alloc(js)1358 static job_t* job_alloc( js )
1359 jobset_t *js;
1360 {
1361 job_id_t max_jobid = 0;
1362 job_t *new_job, *job;
1363 int i;
1364
1365 /*-- Create the command structure --*/
1366 if( (new_job = (job_t*)malloc(sizeof( job_t ))) == NULL )
1367 return NULL;
1368
1369 /*-- Rather inneficiently calculate the next available job_id --*/
1370 for( i = 0; i < js->js_hsize; i++ ) {
1371 for( job = js->js_jobs[i]; job != NULL; job = job->job_nxt )
1372 max_jobid = max(max_jobid, job->job_id);
1373 }
1374
1375 /*-- Create space to hold command-line arguments --*/
1376 if( (new_job->job_args = args_create( 16 )) == NULL ) {
1377 free( new_job );
1378 return NULL;
1379 }
1380
1381 /*-- Initialize all fields --*/
1382 new_job->job_id = max_jobid + 1;
1383 new_job->job_flags = 0;
1384 new_job->job_start = time(NULL);
1385 new_job->job_end = 0;
1386 new_job->job_status = 0;
1387 new_job->job_pid = 0;
1388 new_job->job_output = NULL;
1389 new_job->job_nxt = NULL;
1390
1391 return new_job;
1392 }
1393
1394 /*
1395 * job_free():
1396 *
1397 * If command given by job_id exists within the jobset, js, then
1398 * it is removed and all space allocated to it is reclaimed. True
1399 * is returned upon success, False is returned if job_id does not
1400 * exist within js.
1401 */
job_free(j)1402 static int job_free( j )
1403 job_t *j;
1404 {
1405 if( j == NULL )
1406 return False;
1407
1408 /*
1409 * I don't know if this should go here, but.. if the job may
1410 * have deferred output and we have the name of the defer
1411 * file, then attempt to unlink the file. Ignore the
1412 * results of unlink.
1413 */
1414 if( (j->job_flags & JOB_DEFER) && j->job_output != NULL )
1415 unlink( j->job_output );
1416
1417 if( j->job_args != NULL )
1418 args_destroy( j->job_args );
1419 if( j->job_output != NULL )
1420 free( j->job_output );
1421 free( j );
1422
1423 return True;
1424 }
1425
1426 /*
1427 * jobset_global_init():
1428 *
1429 * Initialize any global variables that may be required by the various
1430 * jobset routines..
1431 */
jobset_global_init()1432 static int jobset_global_init()
1433 {
1434 static int init_done = False;
1435
1436 if( init_done == False ) {
1437 /*
1438 * We need a buffer in which to place the expanded command line
1439 * perior to parsing.
1440 */
1441 if( sg_cmd_buf == NULL ) {
1442 if( (sg_cmd_buf = varbuf_create( 128 )) == NULL ) {
1443 sqsh_set_error( sqsh_get_error(), "varbuf: %s", sqsh_get_errstr() );
1444 return False;
1445 }
1446 }
1447
1448 if( sg_alias_buf == NULL ) {
1449 if( (sg_alias_buf = varbuf_create( 128 )) == NULL ) {
1450 sqsh_set_error( sqsh_get_error(), "varbuf: %s", sqsh_get_errstr() );
1451 return False;
1452 }
1453 }
1454
1455 if (sg_while_buf == NULL)
1456 {
1457 if ((sg_while_buf = varbuf_create( 128 )) == NULL)
1458 {
1459 sqsh_set_error( sqsh_get_error(), "varbuf: %s", sqsh_get_errstr() );
1460 return False;
1461 }
1462 }
1463
1464 init_done = True;
1465 }
1466 return True;
1467 }
1468