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.
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.
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]
23 * Copyright (c) 2011 Gary Mills
25 * Copyright 2009 Sun Microsystems, Inc. All rights reserved.
26 * Use is subject to license terms.
27 * Copyright (c) 2016 by Delphix. All rights reserved.
30 /* Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T */
31 /* All Rights Reserved */
33 #include <sys/resource.h>
35 #include <sys/types.h>
58 #define TMPFILE "_at" /* prefix for temporary files */
60 * Mode for creating files in ATDIR.
61 * Setuid bit on so that if an owner of a file gives that file
62 * away to someone else, the setuid bit will no longer be set.
63 * If this happens, atrun will not execute the file
65 #define ATMODE (S_ISUID | S_IRUSR | S_IRGRP | S_IROTH)
66 #define ROOT 0 /* user-id of super-user */
67 #define MAXTRYS 100 /* max trys to create at job file */
69 #define BADTIME "bad time specification"
70 #define BADQUEUE "queue name must be a single character a-z"
71 #define NOTCQUEUE "queue c is reserved for cron entries"
72 #define BADSHELL "because your login shell isn't /usr/bin/sh,"\
74 #define WARNSHELL "commands will be executed using %s\n"
75 #define CANTCD "can't change directory to the at directory"
76 #define CANTCHOWN "can't change the owner of your job to you"
77 #define CANTCHUID "can't change user identifier"
78 #define CANTCREATE "can't create a job for you"
79 #define INVALIDUSER "you are not a valid user (no entry in /etc/passwd)"
80 #define NOOPENDIR "can't open the at directory"
81 #define NOTALLOWED "you are not authorized to use at. Sorry."
83 "usage: at [-c|-k|-s] [-m] [-f file] [-p project] [-q queuename] "\
85 " at [-c|-k|-s] [-m] [-f file] [-p project] [-q queuename] "\
87 " at -l [-p project] [-q queuename] [at_job_id...]\n"\
88 " at -r at_job_id ...\n"
90 #define FORMAT "%a %b %e %H:%M:%S %Y"
93 static int atoi_for2(char *);
94 static int check_queue(char *, int);
95 static int list_jobs(int, char **, int, int);
96 static int remove_jobs(int, char **, char *);
97 static void usage(void);
98 static void catch(int);
99 static void copy(char *, FILE *, int);
100 static void atime(struct tm
*, struct tm
*);
101 static int not_this_project(char *);
102 static char *mkjobname(time_t);
103 static time_t parse_time(char *);
104 static time_t gtime(struct tm
*);
105 static void escapestr(const char *);
106 void atabort(char *)__NORETURN
;
108 extern int yyparse(void);
110 extern void audit_at_delete(char *, char *, int);
111 extern int audit_at_create(char *, int);
112 extern int audit_cron_is_anc_name(char *);
113 extern int audit_cron_delete_anc_file(char *, char *);
116 * Error in getdate(3G)
118 static char *errlist
[] = {
120 /* 1 */ "getdate: The DATEMSK environment variable is not set",
121 /* 2 */ "getdate: Error on \"open\" of the template file",
122 /* 3 */ "getdate: Error on \"stat\" of the template file",
123 /* 4 */ "getdate: The template file is not a regular file",
124 /* 5 */ "getdate: An error is encountered while reading the template",
125 /* 6 */ "getdate: Malloc(3C) failed",
126 /* 7 */ "getdate: There is no line in the template that matches the input",
127 /* 8 */ "getdate: Invalid input specification"
131 int mday
[12] = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
133 struct tm
*tp
, at
, rt
;
134 static int cshflag
= 0;
135 static int kshflag
= 0;
136 static int shflag
= 0;
137 static int mflag
= 0;
138 static int pflag
= 0;
141 static char pname
[80];
142 static char pname1
[80];
143 static short jobtype
= ATEVENT
; /* set to 1 if batch job */
145 extern int per_errno
;
146 static projid_t project
;
149 main(int argc
, char **argv
)
164 char *jobfile
= NULL
; /* file containing job to be run */
165 char argpbuf
[LINE_MAX
], timebuf
[80];
170 struct project prj
, *pprj
;
171 char mybuf
[PROJECT_BUFSZ
];
172 char ipbuf
[PROJECT_BUFSZ
];
174 (void) setlocale(LC_ALL
, "");
175 #if !defined(TEXT_DOMAIN) /* Should be defined by cc -D */
176 #define TEXT_DOMAIN "SYS_TEST" /* Use this only if it weren't */
178 (void) textdomain(TEXT_DOMAIN
);
181 login
= getuser(user
);
186 atabort(INVALIDUSER
);
189 if (!allowed(login
, ATALLOW
, ATDENY
))
192 while ((c
= getopt(argc
, argv
, "cklmsrf:p:q:t:")) != EOF
)
213 if ((pprj
= getprojbyname(proj
, pprj
,
214 (void *)&mybuf
, sizeof (mybuf
))) != NULL
) {
215 project
= pprj
->pj_projid
;
216 if (inproj(login
, pprj
->pj_name
,
217 (void *)&ipbuf
, sizeof (ipbuf
)))
220 (void) fprintf(stderr
,
221 gettext("at: user %s is "
223 "project %s (%d)\n"),
224 login
, pprj
->pj_name
,
231 if (isdigit(proj
[0]) &&
232 (pprj
= getprojbyid(atoi(proj
), pprj
,
233 (void *)&mybuf
, sizeof (mybuf
))) != NULL
) {
234 project
= pprj
->pj_projid
;
235 if (inproj(login
, pprj
->pj_name
,
236 (void *)&ipbuf
, sizeof (ipbuf
)))
239 (void) fprintf(stderr
,
240 gettext("at: user %s is "
242 "project %s (%d)\n"),
243 login
, pprj
->pj_name
,
249 (void) fprintf(stderr
, gettext("at: project "
250 "%s not found.\n"), proj
);
255 if (optarg
[1] != '\0')
257 jobtype
= *optarg
- 'a';
258 if ((jobtype
< 0) || (jobtype
> 25))
271 when
= parse_time(optarg
);
280 if (lflag
+ rflag
> 1)
284 if (cshflag
|| kshflag
|| shflag
|| mflag
||
285 fflag
|| tflag
|| rflag
)
287 return (list_jobs(argc
, argv
, qflag
, jobtype
));
291 if (cshflag
|| kshflag
|| shflag
|| mflag
||
292 fflag
|| tflag
|| qflag
)
294 return (remove_jobs(argc
, argv
, login
));
297 if ((argc
+ tflag
== 0) && (jobtype
!= BATCHEVENT
))
300 if (cshflag
+ kshflag
+ shflag
> 1)
301 atabort("ambiguous shell request");
305 if (jobtype
== BATCHEVENT
)
308 if (when
== 0) { /* figure out what time to run the job */
309 int argplen
= sizeof (argpbuf
) - 1;
315 /* guard against buffer overflow */
316 argplen
-= strlen(argv
[i
]) + 1;
320 strcat(argp
, argv
[i
]);
324 if ((file
= getenv("DATEMSK")) == 0 || file
[0] == '\0') {
325 tp
= localtime(&now
);
327 * Fix for 1047182 - we have to let yyparse
328 * check bounds on mday[] first, then fixup
329 * the leap year case.
333 mday
[1] = 28 + leap(at
.tm_year
);
335 if (at
.tm_mday
> mday
[at
.tm_mon
])
342 if (localtime(&when
)->tm_isdst
)
343 when
-= (timezone
-altzone
);
345 } else { /* DATEMSK is set */
346 if ((ct
= getdate(argpbuf
)) == NULL
)
347 atabort(errlist
[getdate_err
]);
353 if (when
< now
) /* time has already past */
356 tflen
= strlen(ATDIR
) + 1 + strlen(TMPFILE
) +
357 10 + 1; /* 10 for an INT_MAX pid */
358 tfname
= xmalloc(tflen
);
359 snprintf(tfname
, tflen
, "%s/%s%d", ATDIR
, TMPFILE
, getpid());
361 /* catch INT, HUP, TERM and QUIT signals */
362 if (signal(SIGINT
, catch) == SIG_IGN
)
363 signal(SIGINT
, SIG_IGN
);
364 if (signal(SIGHUP
, catch) == SIG_IGN
)
365 signal(SIGHUP
, SIG_IGN
);
366 if (signal(SIGQUIT
, catch) == SIG_IGN
)
367 signal(SIGQUIT
, SIG_IGN
);
368 if (signal(SIGTERM
, catch) == SIG_IGN
)
369 signal(SIGTERM
, SIG_IGN
);
370 if ((fd
= open(tfname
, O_CREAT
|O_EXCL
|O_WRONLY
, ATMODE
)) < 0)
372 if (chown(tfname
, user
, getgid()) == -1) {
379 sprintf(pname
, "%s", PROTO
);
380 sprintf(pname1
, "%s.%c", PROTO
, 'a'+jobtype
);
383 * Open the input file with the user's permissions.
385 if (jobfile
!= NULL
) {
386 if ((seteuid(user
) < 0) ||
387 (inputfile
= fopen(jobfile
, "r")) == NULL
) {
389 fprintf(stderr
, "at: %s: %s\n", jobfile
, errmsg(errno
));
397 copy(jobfile
, inputfile
, when
);
398 while (rename(tfname
, job
= mkjobname(when
)) == -1) {
400 if (++try > MAXTRYS
/ 10) {
406 if (audit_at_create(job
, 0))
409 cron_sendmsg(ADD
, login
, strrchr(job
, '/')+1, AT
);
411 fprintf(stderr
, gettext(WARNSHELL
), Shell
);
412 cftime(timebuf
, FORMAT
, &when
);
413 fprintf(stderr
, gettext("job %s at %s\n"),
414 strrchr(job
, '/')+1, timebuf
);
415 if (when
- MINUTE
< HOUR
)
416 fprintf(stderr
, gettext(
417 "at: this job may not be executed at the proper time.\n"));
430 for (i
= 0; i
< MAXTRYS
; i
++) {
431 sprintf(name
, "%s/%ld.%c", ATDIR
, t
, 'a'+jobtype
);
432 /* fix for 1099183, 1116833 - create file here, avoid race */
433 if ((fd
= open(name
, O_CREAT
| O_EXCL
, ATMODE
)) > 0) {
439 atabort("queue full");
456 fprintf(stderr
, "at: %s\n", gettext(msg
));
474 * add time structures logically
477 atime(struct tm
*a
, struct tm
*b
)
479 if ((a
->tm_sec
+= b
->tm_sec
) >= 60) {
480 b
->tm_min
+= a
->tm_sec
/ 60;
483 if ((a
->tm_min
+= b
->tm_min
) >= 60) {
484 b
->tm_hour
+= a
->tm_min
/ 60;
487 if ((a
->tm_hour
+= b
->tm_hour
) >= 24) {
488 b
->tm_mday
+= a
->tm_hour
/ 24;
491 a
->tm_year
+= b
->tm_year
;
492 if ((a
->tm_mon
+= b
->tm_mon
) >= 12) {
493 a
->tm_year
+= a
->tm_mon
/ 12;
496 a
->tm_mday
+= b
->tm_mday
;
497 mday
[1] = 28 + leap(a
->tm_year
);
498 while (a
->tm_mday
> mday
[a
->tm_mon
]) {
499 a
->tm_mday
-= mday
[a
->tm_mon
++];
500 if (a
->tm_mon
> 11) {
502 mday
[1] = 28 + leap(++a
->tm_year
);
511 return (isleap(year
+ TM_YEAR_BASE
));
515 * return time from time structure
524 {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
528 for (i
= 1970; i
!= tptr
->tm_year
+TM_YEAR_BASE
; i
++)
529 tv
+= (365 + isleap(i
));
531 * We call isleap since leap() adds
532 * 1900 onto any value passed
535 if (!leap(tptr
->tm_year
) && at
.tm_mday
== 29 && at
.tm_mon
== 1)
536 atabort("bad date - not a leap year");
538 if ((leap(tptr
->tm_year
)) && tptr
->tm_mon
>= 2)
541 for (i
= 0; i
< tptr
->tm_mon
; ++i
)
543 tv
+= tptr
->tm_mday
- 1;
544 tv
= 24 * tv
+ tptr
->tm_hour
;
545 tv
= 60 * tv
+ tptr
->tm_min
;
546 tv
= 60 * tv
+ tptr
->tm_sec
;
551 * Escape a string to be used inside the job shell script.
554 escapestr(const char *str
)
557 (void) putchar('\'');
558 while ((c
= *str
++) != '\0') {
562 (void) fputs("'\\''", stdout
); /* ' -> '\'' */
564 (void) putchar('\'');
568 * make job file from proto + stdin
571 copy(char *jobfile
, FILE *inputfile
, int when
)
576 char dirbuf
[PATH_MAX
+ 1];
581 extern char **environ
;
582 uid_t realusr
, effeusr
;
586 struct project prj
, *pprj
;
587 char pbuf
[PROJECT_BUFSZ
];
588 char pbuf2
[PROJECT_BUFSZ
];
593 * If the inputfile is from a tty, then turn on prompting, and
594 * put out a prompt now, instead of waiting for a lot of file
595 * activity to complete.
597 ttyinput
= isatty(fileno(inputfile
));
599 fputs("at> ", stderr
);
605 * Determine what shell we should use to run the job. If the user
606 * didn't explicitly request that their current shell be over-
607 * ridden (shflag or cshflag), then we use the current shell.
610 Shell
= shell
= "/bin/csh";
612 Shell
= shell
= "/bin/ksh";
615 Shell
= shell
= "/bin/sh";
617 } else if (((Shell
= val
= getenv("SHELL")) != NULL
) &&
620 if ((strstr(val
, "/sh") != NULL
) ||
621 (strstr(val
, "/ksh") != NULL
))
624 /* SHELL is NULL or unset, therefore use default */
625 Shell
= shell
= _PATH_BSHELL
;
629 printf(": %s job\n", jobtype
? "batch" : "at");
630 printf(": jobname: %.127s\n", (jobfile
== NULL
) ? "stdin" : jobfile
);
631 printf(": notify by mail: %s\n", (mflag
) ? "yes" : "no");
634 (void) printf(": project: %d\n", project
);
637 * Check if current user is a member of current project.
638 * This check is done here to avoid setproject() failure
639 * later when the job gets executed. If current user does
640 * not belong to current project, user's default project
641 * will be used instead. This is achieved by not specifying
642 * the project (": project: <project>\n") in the job file.
644 if ((user
= getuser(getuid())) == NULL
)
645 atabort(INVALIDUSER
);
646 project
= getprojid();
647 pprj
= getprojbyid(project
, &prj
, pbuf
, sizeof (pbuf
));
649 if (inproj(user
, pprj
->pj_name
, pbuf2
, sizeof (pbuf2
)))
650 (void) printf(": project: %d\n", project
);
654 for (ep
= environ
; *ep
; ep
++) {
655 if ((val
= strchr(*ep
, '=')) == NULL
)
658 (void) printf("export %s; %s=", *ep
, *ep
);
660 (void) putchar('\n');
663 if ((pfp
= fopen(pname1
, "r")) == NULL
&&
664 (pfp
= fopen(pname
, "r")) == NULL
)
665 atabort("no prototype");
667 * Put in a line to run the proper shell using the rest of
668 * the file as input. Note that 'exec'ing the shell will
669 * cause sh() to leave a /tmp/sh### file around. (1053807)
671 printf("%s << '...the rest of this file is shell input'\n", shell
);
674 while ((c
= getc(pfp
)) != EOF
) {
677 else switch (c
= getc(pfp
)) {
682 * Must obtain current working directory as the user
688 /* change euid for getcwd */
689 if (seteuid(realusr
) < 0) {
692 if (getcwd(dirbuf
, sizeof (dirbuf
)) == NULL
) {
694 "can't obtain current working directory");
696 /* change back afterwards */
697 if (seteuid(effeusr
) < 0) {
707 if (getrlimit(RLIMIT_FSIZE
, &rlp
) == 0) {
708 if (rlp
.rlim_cur
== RLIM_INFINITY
)
709 printf("ulimit unlimited\n");
711 printf("ulimit %lld\n",
716 * fix for 1113572 - use fputs() so that a
717 * newline isn't appended to the one returned
718 * with fgets(); 1099381 - prompt for input.
720 while (fgets(line
, LINE_MAX
, inputfile
) != NULL
) {
723 fputs("at> ", stderr
);
725 if (ttyinput
) /* clean up the final output */
726 fputs("<EOT>\n", stderr
);
729 printf(":%lu", when
);
741 remove_jobs(int argc
, char **argv
, char *login
)
742 /* remove jobs that are specified */
751 atabort("Invalid user.\n");
756 if (chdir(ATDIR
) == -1)
758 for (i
= 0; i
< argc
; i
++)
759 if (strchr(argv
[i
], '/') != NULL
) {
760 fprintf(stderr
, "at: %s: not a valid job-id\n",
762 } else if (stat(argv
[i
], &buf
)) {
763 fprintf(stderr
, "at: %s: ", argv
[i
]);
765 } else if ((user
!= buf
.st_uid
) &&
766 (!cron_admin(pw
->pw_name
))) {
767 fprintf(stderr
, "at: you don't own %s\n",
771 if (cron_admin(pw
->pw_name
)) {
772 login
= getuser((uid_t
)buf
.st_uid
);
777 atabort(INVALIDUSER
);
780 cron_sendmsg(DELETE
, login
, argv
[i
], AT
);
782 audit_at_delete(argv
[i
], ATDIR
, r
);
790 list_jobs(int argc
, char **argv
, int qflag
, int queue
)
795 char *patdir
, *atdir
, *ptr
;
798 struct stat buf
, st1
, st2
;
799 struct dirent
*dentry
;
801 unsigned int atdirlen
;
803 struct passwd
*pwd
, pwds
;
805 char job_file
[PATH_MAX
];
807 getpwuid_r(user
, &pwds
, buf_pwd
, sizeof (buf_pwd
), &pwd
);
809 atabort("Invalid user.\n");
812 /* list jobs for user */
813 if (chdir(ATDIR
) == -1)
816 atdirlen
= strlen(ATDIR
);
817 atdir
= xmalloc(atdirlen
+ 1);
818 strcpy(atdir
, ATDIR
);
819 patdir
= strrchr(atdir
, '/');
822 /* list all jobs for a user */
823 if (stat(ATDIR
, &st1
) != 0 || stat(atdir
, &st2
) != 0)
824 atabort("Can not get status of spooling"
826 if ((dir
= opendir(ATDIR
)) == NULL
)
829 if ((dentry
= readdir(dir
)) == NULL
)
831 if ((dentry
->d_ino
== st1
.st_ino
) ||
832 (dentry
->d_ino
== st2
.st_ino
))
834 if ((r
= audit_cron_is_anc_name(dentry
->d_name
)) == 1)
836 if (stat(dentry
->d_name
, &buf
)) {
837 unlink(dentry
->d_name
);
838 audit_cron_delete_anc_file(dentry
->d_name
,
842 if ((!cron_admin(pwd
->pw_name
)) &&
843 (buf
.st_uid
!= user
))
845 ptr
= dentry
->d_name
;
846 if (((t
= num(&ptr
)) == 0) || (*ptr
!= '.'))
848 strcpy(job_file
, patdir
);
849 strcat(job_file
, dentry
->d_name
);
850 if (pflag
&& not_this_project(job_file
))
852 ascftime(timebuf
, FORMAT
, localtime(&t
));
853 if ((cron_admin(pwd
->pw_name
)) &&
854 ((pw
= getpwuid(buf
.st_uid
)) != NULL
)) {
855 if (!qflag
|| (qflag
&&
856 check_queue(ptr
, queue
)))
857 printf("user = %s\t%s\t%s\n",
858 pw
->pw_name
, dentry
->d_name
,
861 if (!qflag
|| (qflag
&&
862 check_queue(ptr
, queue
)))
864 dentry
->d_name
, timebuf
);
866 (void) closedir(dir
);
867 } else /* list particular jobs for user */
868 for (i
= 0; i
< argc
; i
++) {
870 strlcpy(job_file
, patdir
, PATH_MAX
);
871 strlcat(job_file
, ptr
, PATH_MAX
);
872 if (((t
= num(&ptr
)) == 0) || (*ptr
!= '.')) {
873 fprintf(stderr
, gettext(
874 "at: invalid job name %s\n"), argv
[i
]);
876 } else if (stat(argv
[i
], &buf
)) {
877 fprintf(stderr
, "at: %s: ", argv
[i
]);
880 } else if ((user
!= buf
.st_uid
) &&
881 (!cron_admin(pwd
->pw_name
))) {
882 fprintf(stderr
, gettext(
883 "at: you don't own %s\n"), argv
[i
]);
885 } else if (pflag
&& not_this_project(job_file
)) {
888 if (!qflag
|| (qflag
&&
889 check_queue(ptr
, queue
))) {
890 ascftime(timebuf
, FORMAT
,
892 printf("%s\t%s\n", argv
[i
], timebuf
);
900 * open the command file and read the project id line
901 * compare to the project number provided via -p on the command line
902 * return 0 if they match, 1 if they don't match or an error occurs.
904 #define SKIPCOUNT 3 /* lines to skip to get to project line in file */
907 not_this_project(char *filename
)
913 if ((fp
= fopen(filename
, "r")) == NULL
)
916 for (i
= 0; i
< SKIPCOUNT
; i
++)
917 fscanf(fp
, "%*[^\n]\n");
919 fscanf(fp
, ": project: %d\n", &sproj
);
922 return (sproj
== project
? 0 : 1);
926 check_queue(char *name
, int queue
)
928 if ((name
[strlen(name
) - 1] - 'a') == queue
)
944 * time in the following format (defined by the touch(1) spec):
945 * [[CC]YY]MMDDhhmm[.SS]
947 if ((p
= strchr(t
, '.')) != NULL
) {
948 if (strchr(p
+1, '.') != NULL
)
950 seconds
= atoi_for2(p
+1);
954 memset(&tm
, 0, sizeof (struct tm
));
956 tm
.tm_year
= localtime(&when
)->tm_year
;
959 case 12: /* CCYYMMDDhhmm */
960 century
= atoi_for2(t
);
962 case 10: /* YYMMDDhhmm */
963 tm
.tm_year
= atoi_for2(t
);
969 tm
.tm_year
+= (century
- 19) * 100;
970 case 8: /* MMDDhhmm */
971 tm
.tm_mon
= atoi_for2(t
) - 1;
973 tm
.tm_mday
= atoi_for2(t
);
975 tm
.tm_hour
= atoi_for2(t
);
977 tm
.tm_min
= atoi_for2(t
);
985 if ((when
= mktime(&tm
)) == -1)
988 when
-= (timezone
-altzone
);
996 value
= (*p
- '0') * 10 + *(p
+1) - '0';
997 if ((value
< 0) || (value
> 99))
1005 fprintf(stderr
, USAGE
);