/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "pty.h"
#define NEED_CHILD_INFO_BEFORE_REAP 0
const char* progname;
static int got_sigchld = 0;
static int allocated_pty_fd = -1;
static struct tms start_tms;
static struct tms end_tms;
static clock_t start = 0;
static clock_t end;
// Local functions declarations
#if NEED_CHILD_INFO_BEFORE_REAP
static void sigchld(
int sig, siginfo_t* info,
void* uap);
#endif //NEED_CHILD_INFO_BEFORE_REAP
static void sigcont(
int sig);
static void modify_term(
int slavefd,
int echooff,
int fixerase);
static void do_report(
const char* fname,
int status,
struct rusage* rusage
/*prusage_t* usage,*/ /*psinfo_t* psinfo,*/);
int main(
int argc,
char** argv) {
int nopt;
options_t params;
// Get program name - this is used in error.c, for example
progname = basename(argv[0]);
// Parse options
nopt = readopts(argc, argv, ¶ms);
// If dumpenv was requested just do it and we are done
if (nopt == argc && params.envfile != NULL) {
return dumpenv(params.envfile);
}
pid_t pid;
// Bootstrap env
if (params.envfile != NULL) {
pid_t p = fork();
if (p < 0) {
err_sys(
"fork error");
}
if (p == 0) {
char** env = readenv(params.envfile);
execve(argv[0], argv, env);
err_sys(
"execve failed");
}
int status, w;
w = waitpid(p, &status, WUNTRACED | WCONTINUED);
if (w != -1 && WIFEXITED(status)) {
exit(WEXITSTATUS(status));
}
exit(EXIT_FAILURE);
}
argv += nopt;
argc -= nopt;
/* now argv points to the executable */
if (argc == 0) {
// -e turned echoing off
// -w wait until signaled (SIGCONT) before executing a process
// -p define pts_name to use instead of opening a new one
// --env passes additional environment variable to a program
// in NAME=VALUE form. For multiple variables multiple
// --env options should be used.
// --no-pty do not allocate pseudo-terminal - just execute a program
// if --no-pty is used, -p/-w has no effect
// --readenv start a new process in an environment. recorded in a
// specified file.
// --dumpenv record current environment in a specified file
// --report record process' and exit status information into
// specified file
err_quit(
"\n\n"
"usage: %s [-e] [--no-pty] [-w] [-p pts_name] [--set-erase-key]\n"
"\t\t[--readenv env_file] [[--env NAME=VALUE] ...] [--dir dir]\n"
"\t\t[--redirect-error] [--report report_file] program [ arg ... ]\n\n"
"\t-e\t\t turn echoing off\n"
"\t-w\t\t wait SIGCONT after reporting PID/TTY/.. but before executing the program\n"
"\t-p\t\t define pts_name to attach process's I/O instead of opening a new one\n"
"\t--readenv\t read environment to start process in from a file\n"
"\t--env\t\t pass (additional) environment variable to the process\n"
"\t--dir\t\t change working directory for starting process\n"
"\t--redirect-error redirect stderror to stdout for starting process (makes sense if --no-pty only)\n"
"\t--report\t record process' and exit status information into specified file\n\n"
"usage: %s --dumpenv env_file\n"
"\t--dumpenv\t dump environment to a file\n"
, progname, progname);
exit(-1);
}
// Set SIGCONT handler for both parent and child
// This is to avoid a race condition
signal(SIGCONT, sigcont);
if (params.redirect_error) {
close(STDERR_FILENO);
dup2(STDOUT_FILENO, STDERR_FILENO);
}
if (params.nopty == 0) {
if (params.pty != NULL) {
pid = pty_fork1(params.pty);
}
else {
pid = pty_fork(&allocated_pty_fd);
}
}
else {
pid = fork();
// We call setsid for a child to make it a session leader -
// this allows us later send signals to the whole session
// But we don't want to do this in a single case - when --no-pty is
// used but we do have terminal (because in this case the connection
// with existent terminal will be lost and it will not be a controlling
// terminal for the process anymore.
if (!isatty(STDOUT_FILENO)) {
setsid();
}
}
if (pid < 0) {
err_sys(
"fork error");
}
if (pid == 0) {
/* child */
/*
* get a name of a terminal we have
*/
if (isatty(STDOUT_FILENO)) {
params.pty = ttyname(STDOUT_FILENO);
}
// MAGIC variable to be used to find all children of this process
char magic[20];
snprintf(magic, 20,
"NBMAGIC=%ld", (
long) getppid());
printf(
"PID=%d\n", getpid());
printf(
"TTY=%s\n", params.pty == NULL ?
"null" : params.pty);
printf(
"PSID=%d\n", getsid(0));
if (params.reportfile != NULL) {
printf(
"REPORT=%s.%ld\n", params.reportfile, (
long) getppid());
}
printf(
"%s\n", magic);
printf(
"\n");
// empty line means that we printed everything we needed
fflush(stdout);
modify_term(STDIN_FILENO, params.noecho, params.set_erase_key);
// Set passed environment variables
for (
int i = 0; i < params.envnum; i++) {
putenv(params.envvars[i]);
}
// The last variable we set is NBMAGIC
putenv(magic);
if (params.waitSignal) {
/*
* Waiting for a SIGCONT, blocking (queuing) all other signals.
* (but let SIGINT to pass...)
*/
sigset_t sigset;
sigfillset(&sigset);
sigdelset(&sigset, SIGCONT);
sigdelset(&sigset, SIGINT);
sigsuspend(&sigset);
}
else {
// Reset SIGCONT handler to default
signal(SIGCONT, SIG_DFL);
}
// Just before doing execvp - notify the parent process so that
// it can take a start time snapshot
// Parent doesn't explicitly wait for this signal, but guarantees that
// it is not blocked
kill(getppid(), SIGCONT);
if (params.wdir != NULL) {
if (chdir(params.wdir) == -1) {
err_sys2(errno == ENOENT ? 127 : 1,
"failed to change directory: %s", params.wdir);
}
}
// setvbuf is not retained across an exec, (as buffering is a property
// of a FILE, not fd - so need to do un-buffering in the execd process.
// Use LD_PRELOAD for this..
// This is controlled by java code for now, at least
if (execvp(argv[0], argv) < 0) {
err_sys2(errno == ENOENT ? 127 : 1,
"failed to start %s", argv[0]);
}
}
/* parent */
/*
* We will ignore SIGINT and SIGQUIT signals generated by a terminal to
* have a chance to get user's program exit status...
*/
signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
if (params.envvars != NULL) {
free(params.envvars);
}
#if NEED_CHILD_INFO_BEFORE_REAP
/*
* We want to get a statistics of the child we just forked.
* This statistics will be collected differently on different systems,
* but this should be done at the very end of the process execution -
* (just after it is done, but before reaping it's exit status).
* The approach is to set a signal handler for SIGCHLD and wait for it.
* SIGCHLD is send when child has finished it's execution (!!! actually,
* when it changes it's state - so it is send when it goes to
* foreground/background or when it is stoped/continued !!!)
* Once signal is received - will read process's /proc and will get/reap it's
* status with waitpid.
*/
struct sigaction sig_action;
sig_action.sa_sigaction = sigchld;
sigemptyset(&sig_action.sa_mask);
sig_action.sa_flags = SA_SIGINFO | SA_RESETHAND | SA_NODEFER | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sig_action, NULL);
#endif // NEED_CHILD_INFO_BEFORE_REAP
/*
* If we have a newly allocated terminal - start a loop that re-directs
* process's I/O to a terminal.
*/
if (allocated_pty_fd > 0) {
// At least on Windows, when gdb is started through this pty process
// and calling process is killed (i.e. stdin gets broken)
// and even when we close master_fd, gdb continues to work...
// ??? will kill the process (gdb) in this case...
int loop_result = loop(allocated_pty_fd);
/* copies stdin -> ptym, ptym -> stdout */
if (loop_result != 0) {
int attempt = 2;
while (attempt-- >= 0 && kill(pid, 0) == 0) {
kill(pid, SIGTERM);
sleep(1);
}
if (kill(pid, 0) == 0) {
kill(pid, SIGKILL);
}
}
}
#if NEED_CHILD_INFO_BEFORE_REAP
/*
* There is a race here !!
* I bet it could be that got_sigchld is 0, but signal is already send
* ?? What is we send SIGKILL to the child (the code above)? - seems that in
* this case signal is sent as well...
*
* Will hung on a SIGCHLD if signal was not already received only
*/
while (!got_sigchld) {
sigset_t sigset;
sigfillset(&sigset);
sigdelset(&sigset, SIGCHLD);
// Do not block SIGCONT as this is a way for child to notify the parent
// about user process starting moment
sigdelset(&sigset, SIGCONT);
sigsuspend(&sigset);
}
//prusage_t usage;
//psinfo_t psinfo;
if (params.reportfile != NULL) {
// int pfd;
// char fname[PATH_MAX];
// int r = getrusage(RUSAGE_CHILDREN, &rusage);
// printf("r==%d\n", r);
// snprintf(fname, PATH_MAX, "/proc/%ld/usage", (long) pid);
// pfd = open(fname, O_RDONLY);
// read(pfd, &usage, sizeof (prusage_t));
// close(pfd);
// snprintf(fname, PATH_MAX, "/proc/%ld/psinfo", (long)pid);
// pfd = open(fname, O_RDONLY);
// read(pfd, &psinfo, sizeof (psinfo_t));
// close(pfd);
}
#endif //NEED_CHILD_INFO_BEFORE_REAP
// Reap the process once it's /proc is read...
int status, w;
while ((w = wait(&status)) != pid);
#if !NEED_CHILD_INFO_BEFORE_REAP
end = times(&end_tms);
#endif
struct rusage rusage;
struct rusage* prusage = &rusage;
if (params.reportfile) {
if (getrusage(RUSAGE_CHILDREN, prusage) == -1) {
prusage = NULL;
}
}
else {
prusage = NULL;
}
if (allocated_pty_fd > 0) {
tcdrain(allocated_pty_fd);
}
if (w != -1) {
if (params.reportfile != NULL) {
int sz = strlen(params.reportfile) + 10;
char reportfile[sz];
snprintf(reportfile, sz,
"%s.%ld", params.reportfile, (
long) getpid());
do_report(reportfile, status, prusage
/*&usage, &psinfo,*/);
}
if (WIFEXITED(status)) {
exit(WEXITSTATUS(status));
}
}
exit(EXIT_FAILURE);
}
void do_report(
const char* fname,
int status,
struct rusage* rusage
/*prusage_t* usage, *psinfo_t* psinfo,*/) {
int fd = open(fname, O_TRUNC | O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR);
if (fd > 0) {
char info[100];
sprintf(info,
"RC: %d\n", WIFEXITED(status) ? WEXITSTATUS(status) : 1);
writen(fd, info, strlen(info));
sprintf(info,
"SIG: %s\n", WIFSIGNALED(status) ? strsignal(WTERMSIG(status)) :
"-");
writen(fd, info, strlen(info));
sprintf(info,
"CORE: %d\n", WCOREDUMP(status) ? 1 : 0);
writen(fd, info, strlen(info));
double tps = (
double) sysconf(_SC_CLK_TCK);
sprintf(info,
"RTIME: %ld\n", start == 0 ? 0 : (
long) ((1000 * (end - start)) / tps));
writen(fd, info, strlen(info));
if (end_tms.tms_cutime > 0) {
sprintf(info,
"UTIME: %ld\n", (
long) ((1000 * (end_tms.tms_cutime - start_tms.tms_cutim
e)) / tps));
writen(fd, info, strlen(info));
sprintf(info, "STIME: %ld\n", (long) ((1000 * (end_tms.tms_cstime - start_tms.tms_cstime)) / tps));
writen(fd, info, strlen(info));
}
if (rusage != NULL) {
if (end_tms.tms_cutime == 0) {
sprintf(info, "UTIME: %ld\n", (long) (rusage->ru_utime.tv_sec + rusage->ru_utime.tv_usec / 1000000.0));
writen(fd, info, strlen(info));
sprintf(info, "STIME: %ld\n", (long) (rusage->ru_stime.tv_sec + rusage->ru_stime.tv_usec / 1000000.0));
writen(fd, info, strlen(info));
}
sprintf(info, "MAXRSS: %ld\n", rusage->ru_maxrss);
writen(fd, info, strlen(info));
}
// if (psinfo != NULL) {
// sprintf(info, "RSS_KB: %lu\n", psinfo == NULL ? 0 : psinfo->pr_rssize);
// writen(fd, info, strlen(info));
// }
close(fd);
} else {
warn_sys("Failed to write status to %s", fname);
}
}
/**
* turn echo off (for slave pty)
* set erase to ^H
*/
static void modify_term(int fd, int echooff, int fixerase) {
if (echooff == 0 && fixerase == 0) {
return;
}
struct termios stermios;
if (tcgetattr(fd, &stermios) < 0) {
err_sys("tcgetattr error");
}
if (echooff) {
stermios.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
/*
* Also turn off NL to CR/NL mapping on output.
*/
stermios.c_oflag &= ~(ONLCR);
}
if (fixerase) {
stermios.c_cc[VERASE] = (cc_t) '\b';
}
if (tcsetattr(fd, TCSANOW, &stermios) < 0) {
err_sys("tcsetattr error");
}
}
static void sigcont(int sig) {
start = times(&start_tms);
}
#if NEED_CHILD_INFO_BEFORE_REAP
static void sigchld(int sig, siginfo_t* info, void* uap) {
end = times(&end_tms);
got_sigchld = 1;
}
#endif //NEED_CHILD_INFO_BEFORE_REAP