In my C program, I am creating a child process and running execvp in the child. But I'm trying to change the error message to something else, for the execvp command (if there was an error).
I know that if it returned, then it was an error, then I can print my own custom error message on the next line. That's one type of error that can occur, for example this happens if I give the command "sdfsd" to execvp. This part is working for me.
But if I type, "find sdfsd" then it does not return and prints "find: `sdfsd': No such file or directory".
I want to change this message (and essentially any kind of error message coming from exevcp) to my own custom one.
I believe I can use dup2 to do this, but I'm not sure how...
In the child process I tried
dup2(STDERR_FILENO, 1);
fclose(stderr);
But this just stops the child process from writing any error messages. I still can't print my own message in all cases..
Does anyone know how to do this?
thanks
Since execvp never returns if it successfully starts the new program, you won't be able to print your own error message in the child process after the program run by execvp fails. One option would be to pipe stderr to the parent process, intercept the error message there, and then print your own error message.
For example:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int ff, p[2];
FILE *f;
char *vv[] = {"find", "garbage", (char *)NULL};
char msg[100];
if (pipe(p) != 0)
{
fprintf(stderr, "Pipe failed\n");
return 1;
}
if ((ff = fork()) == -1 )
{
fprintf(stderr, "Fork failed\n");
return 1;
}
if (ff == 0)
{
/* In the child process */
close(2);
close(p[0]);
dup2(p[1], 2);
execvp("find", vv);
return 1;
};
/* In the parent process */
close(p[1]);
f = fdopen(p[0], "r");
if (f == NULL)
{
fprintf(stderr, "Fdopen failed\n");
return 1;
}
if (fgets(msg, sizeof(msg), f) == NULL)
{
fprintf(stderr, "Fgets failed\n");
return 1;
}
printf("Error message was: %s", msg);
/* and so on */
return 0;
}
Related
I have this function that executes a command
int cmd2(char * const *cmd, char * std_out)
{
char tmp[4096];
int pipefds[2], r, status, x;
pid_t pid;
if (pipe(pipefds) == -1){
return -1;
}
if ( (pid = fork()) == -1){
return -1;
}
if (pid == 0)
{
dup2(pipefds[1], STDOUT_FILENO);
dup2(piepfds[1], STDERR_FILENO);
close(pipefds[0]);
close(pipefds[1]);
execvp(cmd[0] , cmd);
}
else
{
close(pipefds[1]);
x = read(pipefds[0], tmp, 4096);
printf("Got %d bytes\n",x);
wait(NULL);
}
return 0;
}
the error message that should be outputted
┌──(kali㉿kali)-[~/]
└─$ ./wow
zsh: no such file or directory: ./wow
when running another c code that fputs a buffer into stderr it is display by the cmd2 without any problems
I tried to redirect stderr to stdout using 2>&1 but this does not seem to have an effect
how to read any/all results from executing the command
If you try running a non-existent command with cmd2, execvp will set errno and return. It will not print anything anywhere. You need to check errno and print the error. Something as simple as:
execvp(cmd[0], cmd);
perror(cmd[0]);
exit(EXIT_FAILURE);
should usually suffice. zsh does something like that when you try to run a non-existent command with it, that's why you see an error message.
Note however that it is the child process that prints the message. You discard child process output and only print its length. You need to do something about it if you want to see the message.
I have a program that forks a child and want it to communicate with its parent. However, I seem to get an error when closing the write end in the child.
The program stops inside the child and in the if (close(pfd1[1]) == -1)
Apparently it fails when the child wants to close the write end. Why?
/* Note: working under the assumption that the messages are of equal length */
int main(int argc, char * argv[])
{
int pfd1[2];
char buf[BUF_SIZE];
//checks pipefd1
if (pipe(pfd1) == -1)
{
printf("Error opening pipe 1!\n");
exit(1);
}
printf("Pipe opened with success. Forking ...\n");
// child 1
switch (fork())
{
case -1:
printf("Error forking child 1!\n");
exit(1);
case 0:
printf("\nChild 1 executing...\n");
/* close writing end of first pipe */
if (close(pfd1[1]) == -1)
{
printf("Error closing writing end of pipe 1.\n");
_exit(1);
}
/* read from pipe 1 */
if (read(pfd1[0], buf, 2000))
{
printf("Error reading to pipe 1.\n");
_exit(1);
}
/* close reading end of first pipe */
if (close(pfd1[1]) == -1)
{
printf("Error closing writing end of pipe 1.\n");
_exit(1);
}
printf("Message received child ONE: %s", buf);
printf("Exiting child 1...\n");
_exit(0);
default: //parent breaks just out
break;
}
printf("inside parent\n");
int child = 1;
char *message = "Hey child1, this is your parent speaking";
if(child == 1)
{
//close read end of pipe
if(close(pfd1[0]) == -1)
{
printf("Error closing reading end of the pipe.\n");
exit(EXIT_FAILURE);
}
printf("Parent closed read end of pipe1\n");
//read end is closed, now write to child
if(write(pfd1[1], message, strlen(message)))
{
printf("Error writing to the pipe.");
_exit(EXIT_FAILURE);
}
printf("Writing to child1 succeeded\n");
}
if (wait(NULL) == -1)
{
printf("Error waiting.\n");
exit(EXIT_FAILURE);
}
if (wait(NULL) == -1)
{
printf("Error waiting.\n");
exit(EXIT_FAILURE);
}
printf("Parent finishing.\n");
exit(EXIT_SUCCESS);
}
First of all, in the child's case you attempt to close the writing end of the pipe twice. I guess the second call to close(2) was meant to close the reading end, as mentioned in the comment above it:
/* close reading end of first pipe */
if (close(pfd1[0]) == -1)
{
printf("Error closing writing end of pipe 1.\n");
_exit(1);
}
Besides that, note that both read(2) and write(2) return the number of bytes that were actually read or written; in the case of error the return value is -1, so your error-checking conditions there should be fixed too, to something like:
/* read from pipe 1 */
if (read(pfd1[0], buf, 2000) < 0) {
printf("Error reading to pipe 1.\n");
_exit(1);
}
and
//read end is closed, now write to child
if(write(pfd1[1], message, strlen(message)) < 0) {
printf("Error writing to the pipe.");
_exit(EXIT_FAILURE);
}
On the principle of teaching to fish, a good technique to diagnose problems like this is to check what the error was and print a more informative message. Here is a technique I frequently put into a header file and use:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* This declaration and macro would really go into a header file: */
void fatal_error_helper( const char* msg, const char* sourcefile, int lineno, const char* syserr );
#define fatal_system_error(m) \
fatal_error_helper( (m), __FILE__, __LINE__, strerror(errno) )
/* This function definition would really go into a .c file: */
void fatal_error_helper( const char* const msg,
const char* const sourcefile,
const int lineno,
const char * const syserr )
{
fflush(stdout); /* Don't cross the streams! */
fprintf( stderr,
"%s at %s:%d: %s. Program terminated.\n",
msg, sourcefile, lineno, syserr
);
exit(EXIT_FAILURE);
}
/* Test driver: */
FILE* fails_to_open_file( const char* filename )
/* Returns a FILE* to an open file. If the operation fails, prints an
* error message and terminates the program.
*/
{
/* Do this in general before calling the function whose error value
* you check. Otherwise, you might report the wrong error message
* from an earlier call and really confuse someone.
*/
errno = 0;
FILE* result = NULL;
result = fopen(filename, ""); /* Fails. */
if (!result)
fatal_system_error("Failed to open file");
return result;
}
int main(void)
{
fails_to_open_file("nonexistent.file");
return EXIT_SUCCESS;
}
This gives an error message such as: Failed to open file at prog.c:26: Invalid argument. Program terminated.
I am studying for an OS exam on Tuesday. In order to prepare, I am attempting to simulate the command line pipe via a C program.
The program is pretty simple. I make a pipe and then fork a child process.
The child process redirects standard output to the write-end of the pipe, closes the file descriptors for the pipe, and then executes a command (ls, in this case).
The parent process waits for the child process to exit, redirects standard input to the read-end of the pipe, closes the file descriptors for the pipe, and then executes a command (grep 'school', in this case).
When I execute the command via the command line using ls | grep 'school' there is a line that says "school" printed to standard output, which makes sense given that there is a directory in the directory that I am running the program in named that.
When I run the program that I made, I do not receive any error messages, but it does not produce any output.
The only thing I can think of that would prevent this from working is that redirecting standard output in the child process is somehow affecting the output of the parent process's command, but I'm almost positive that that shouldn't be the case.
Here is the code:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main() {
int fds[2];
int pipe_val, close_val, write_val, dup_val, status;
pid_t pid;
char *error;
pipe_val = pipe(fds);
if (pipe_val) {
fprintf(stderr, "Failed to prepare pipe.\n");
return -1;
}
pid = fork();
if (pid == -1) {
fprintf(stderr, "Failed to fork a child process.\n");
return -1;
} else if (pid == 0) {
dup_val = dup2(fds[1], STDOUT_FILENO);
if (dup_val) {
error = strerror(errno);
fprintf(stderr, "Failed to redirect standard output in child process because %s\n", error);
exit(1);
}
close_val = close(fds[0]);
if (close_val) {
fprintf(stderr, "Failed to close read-end of pipe in child process.\n");
exit(1);
}
close_val = close(fds[1]);
if (close_val) {
fprintf(stderr, "Failed to close write-end of pipe in child process.\n");
exit(1);
}
execl("/bin/ls", "ls", NULL);
fprintf(stderr, "Failed to execute command in child process.\n");
exit(1);
} else {
wait(&status);
dup_val = dup2(fds[0], STDIN_FILENO);
if (dup_val) {
error = strerror(errno);
fprintf(stderr, "Failed to redirect standard input in parent process because %s.\n", error);
return -1;
}
close_val = close(fds[0]);
if (close_val) {
fprintf(stderr, "Failed to close read-end of the pipe in the parent process.\n");
return -1;
}
close_val = close(fds[1]);
if (close_val) {
fprintf(stderr, "Failed to close write-end of the pipe in the parent process.\n");
return -1;
}
execl("/bin/grep", "grep", "school", NULL);
fprintf(stderr, "Failed to execute the command in the parent process.\n");
return -1;
}
}
Your first problem is that you haven't included all the necessary headers for the functions you're using. strerror requires <string.h> and wait requires <sys/wait.h>.
If you're compiling with gcc, always use gcc -Wall and read the warnings. In this case it would have complained about the implicit declaration of strerror.
Because strerror wasn't declared, the compiler assumes that it returns an int, which is wrong. If you are running the program on 64-bit Linux x86, int is not even the same size as the pointer returned by strerror. This becomes a fatal problem when you then pass the result of strerror to fprintf with a %s format, because the pointer has been misinterpreted as an int and then converted back to a pointer, ending up with a bogus value. fprintf segfaults and you never see your error message.
Include the right headers and you'll see an error message which will lead you to the next problem you need to fix.
I have 2 processes (an 'ls' process and a 'grep'). I'm using pipe to communicate between both of them. But the grep process is unable to read from the pipe. Could you help me figure out why so?
Here is my code
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int pipe_fd[2];
int main()
{
pid_t p1,p2;
char *prog1_argv[4];
char *prog2_argv[2];
/* Build argument list */
prog1_argv[0] = "ls";
prog1_argv[1] = "-l";
prog1_argv[2] = "/";
prog1_argv[3] = NULL;
prog2_argv[0] = "grep";
prog2_argv[1] = "s";
prog2_argv[1] = NULL;
if (pipe(pipe_fd) < 0)
{
printf ("pipe failed");
}
p1 = fork();
if(p1 == 0)
{
printf("in child\n");
close(pipe_fd[0]);
if(dup2(pipe_fd[1],1)<0)
{
printf("dup failed:%d\n",errno);
}
close(pipe_fd[1]);
if(execvp (prog1_argv[0], prog1_argv)<0)
printf("exec failed");
}
if(p1>0)
{
printf("im in parent\n");
waitpid(p1,NULL,0);
printf("parent: child exited. Now test the pipe\n");
close(pipe_fd[1]);
if(dup2(pipe_fd[0],0)<0)
{
printf("dup failed:%d\n",errno);
}
close(pipe_fd[0]);
if(execvp (prog2_argv[0], prog2_argv)<0)
printf("exec failed");
}
}
Fundamentally, you should not be waiting for the ls to die before running the grep.
The ls command might generate so much data that it can't all be stored in the pipe, so the ls command will block until the other process reads from the pipe, but the other process is waiting for ls to complete before it tries to read anything from the pipe. This is a deadlock.
Also, by waiting like that, you enforce serial execution, which throws away the benefits of multiple cores.
There are a number of minor improvements you should make. There are various points at which you report errors. Errors should be reported on the standard error stream (stderr), not on stdout. You should also ensure the program does not continue after at least some of those errors.
You don't have to test the return value from any of the exec*() system calls. If the function returns, it failed. And again, you should ensure that the process exits after that. In this program, it doesn't matter that the child continues; in many programs, not exiting would lead to chaos (two processes trying to read standard input at the same time, for example).
There's no need for pipe_fd to be a global variable. Do make sure all your messages end with a newline, please. You didn't include <sys/wait.h> so you were working without a prototype in scope for the waitpid() function — that's generally a bad idea. You should set your compiler to fussy so it demands that every function has a prototype in scope before it is used or defined. You can initialize the argument lists in the definitions:
char *prog1_argv[] = { "ls", "-l", "/", NULL };
char *prog2_argv[] = { "grep", "s", NULL };
This has the crucial beneficial side-effect of not zapping prog_argv2[1] with a NULL pointer (as noted by Matthias in his answer. I also removed the sizes of the arrays; the second one was dimensioned at 2 and needed to be 3, but when you initialize like this, the compiler does the counting.
One thing you did correctly that was important to do correctly is ensure that the pipe file descriptors were all closed.
This works correctly for me:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main(void)
{
pid_t p1;
int pipe_fd[2];
char *prog1_argv[] = { "ls", "-l", "/", NULL };
char *prog2_argv[] = { "grep", "s", 0 };
if (pipe(pipe_fd) < 0)
{
fprintf(stderr, "pipe failed:%d\n", errno);
exit(1);
}
p1 = fork();
if (p1 == 0)
{
printf("In child\n");
close(pipe_fd[0]);
if (dup2(pipe_fd[1], 1) < 0)
{
fprintf(stderr, "dup failed:%d\n", errno);
exit(1);
}
close(pipe_fd[1]);
execvp(prog1_argv[0], prog1_argv);
fprintf(stderr, "exec failed:%d\n", errno);
exit(1);
}
if (p1 > 0)
{
printf("In parent\n");
close(pipe_fd[1]);
if (dup2(pipe_fd[0], 0) < 0)
{
fprintf(stderr, "dup failed:%d\n", errno);
exit(1);
}
close(pipe_fd[0]);
execvp(prog2_argv[0], prog2_argv);
fprintf(stderr, "exec failed:%d\n", errno);
exit(1);
}
fprintf(stderr, "Fork failed:%d\n", errno);
return(1);
}
You override your grep's argument. Try:
int main()
{
pid_t p1,p2;
char *prog1_argv[4];
char *prog2_argv[3];
/* Build argument list */
prog1_argv[0] = "ls";
prog1_argv[1] = "-l";
prog1_argv[2] = "/";
prog1_argv[3] = NULL;
prog2_argv[0] = "grep";
prog2_argv[1] = "s";
prog2_argv[2] = NULL;
// ...
Hi I need a little help with parallel download program.
Currently, it is downloading the same file in parallel instead of downloading multiple files at the same time.
Something is wrong with the fork and fgets, not sure how to fix them. Thank you.
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
FILE *file; /*declare the file pointer*/
#define LINE_MAX 1000
char line [LINE_MAX];
//Parent process
int main()
{
pid_t pid;
file= fopen ("urls.txt", "rt"); /*open file and read it*/
if(!file)
{
perror("fopen");
exit(-1);
}
int numberOfChildren = 0;
while (!feof (file)) {
memset (line,'\0',1000);
char *urlPtr;
while (!feof (file))
{
urlPtr= fgets (line,LINE_MAX, file);
if(urlPtr)
{
int lineLen = strlen(urlPtr);
urlPtr[lineLen-1] = '\0';
pid = fork();
++numberOfChildren;
if (pid == 0) { /* child process */
execlp("/usr/bin/wget", "wget", urlPtr, NULL);
}
else if (pid < 0) { /* error occurred */
fprintf(stderr, "Fork Failed");
exit(-1);
}
}
}
while (numberOfChildren>0) { /* parent process */
/* parent will wait for the child to complete */
wait (NULL);
--numberOfChildren;
printf ("Child Complete");
}
}
fclose (file); /*close file command*/
return 0;
}
You have the fork() check outside the URL reading loop. You first read lots of URLs and spawn a lot of children, and then do the pid check. Try
while (!feof (file))
{
urlPtr= fgets (line,LINE_MAX, file);
pid = fork();
if (pid == 0) { /* child process */
execlp("/usr/bin/wget", "wget", urlPtr, NULL);
}
else if (pid < 0) { /* error occurred */
fprintf(stderr, "Fork Failed");
exit(-1);
}
++numberOfChildren;
}
You should put a diagnostic print and exit after the execlp() (but in the child code after the if). You should probably also close the input file before you execute wget; the program doesn't need it open. No huge harm done this time, but it's good to be tidy. Your parent probably shouldn't exit just because one child failed to fork(); you have other children, in general, that you should wait for. You might stop processing the file at that point, though. And you should definitely forget about feof(); use while (fgets(line, sizeof(line), file) != 0), though that means you don't need urlPtr. The memset() is superfluous; fgets() initializes the string correctly.
Adaptation of code in question
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
FILE *file; /*declare the file pointer*/
#define LINE_MAX 1000
char line [LINE_MAX];
//Parent process
int main(void)
{
pid_t pid;
file = fopen("urls.txt", "rt"); /*open file and read it*/
if (!file)
{
perror("fopen");
exit(-1);
}
int numberOfChildren = 0;
memset(line,'\0',1000);
char *urlPtr;
while (!feof(file))
{
urlPtr= fgets(line, sizeof(line), file);
if (urlPtr)
{
int lineLen = strlen(urlPtr);
urlPtr[lineLen-1] = '\0';
pid = fork();
++numberOfChildren;
if (pid == 0)
{ /* child process */
execlp("/usr/bin/wget", "wget", urlPtr, NULL);
fprintf(stderr, "%d: wget failed\n", (int)getpid());
exit(1);
}
else if (pid < 0)
{ /* error occurred */
fprintf(stderr, "Fork Failed\n");
exit(-1);
}
else
printf("%d: %s\n", (int)pid, urlPtr);
}
}
/* JL: Moved block of code */
while (numberOfChildren>0)
{ /* parent process */
/* parent will wait for the child to complete */
int status;
int corpse = wait(&status);
--numberOfChildren;
printf("Child %d Complete (0x%04X)\n", corpse, status);
}
fclose(file); /*close file command*/
return 0;
}
Note that a while (!feof(file)) loop has been removed, but there is more unnecessary code that could go. Given data file
ftp://ftp.iana.org/tz/releases/tzcode2012f.tar.gz
ftp://ftp.iana.org/tz/releases/tzdata2012f.tar.gz
The code above works fetching the two files in parallel.
Alternative code
I like to use functions, even for relatively short stretches of code that are used once. Hence the be_childish() function added below. The error reporting is a bit tedious to write out, but that is no excuse for not doing it.
I briefly introduced a minimal function that does error reporting, based on an elaborate library of my own, but it would only be used twice in this code (for the file open error and after execlp() returns, which always and unconditionally indicates failure), but decided to leave it out. I have functions such as err_setarg0(), err_error(), err_remark() and err_usage() and using those would reduce each error report to a single line (and some more complex functions that could be told to include the PID automatically, etc). To me, it is worth having such a library as it makes error checking much, much simpler and therefore less painful and less likely to be skimped on.
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
static void be_childish(const char *urlPtr)
{
const char *wget = "/usr/bin/wget";
char *nl = strchr(urlPtr, '\n');
if (nl != 0)
*nl = '\0';
printf("%d: %s\n", (int)getpid(), urlPtr);
execlp(wget, "wget", urlPtr, NULL);
fprintf(stderr, "%d: Failed to execute %s\n", (int)getpid(), wget);
exit(EXIT_FAILURE);
}
int main(int argc, char **argv)
{
FILE *file;
char line [1024];
pid_t pid;
const char *name = "urls.txt";
int rc = EXIT_SUCCESS;
if (argc == 2)
name = argv[1];
else if (argc > 2)
{
fprintf(stderr, "Usage: %s [filename]\n", argv[0]);
exit(EXIT_FAILURE);
}
file = fopen(name, "rt"); /* Undefined behaviour per POSIX */
int numberOfChildren = 0;
if (file == 0)
{
fprintf(stderr, "Failed to open file %s\n", name);
exit(EXIT_FAILURE);
}
while (fgets(line, sizeof(line), file) != 0)
{
if ((pid = fork()) == 0)
{
fclose(file);
be_childish(line);
}
else if (pid < 0)
{
fprintf(stderr, "Fork Failed");
rc = EXIT_FAILURE;
break;
}
++numberOfChildren;
}
fclose(file);
/* Parent waits for the children to complete */
while (numberOfChildren > 0)
{
int status;
const char *result = "OK";
pid = wait(&status);
--numberOfChildren;
if (status != 0)
{
result = "Failed";
rc = EXIT_FAILURE;
}
printf("Child %d %s\n", pid, result);
}
return rc;
}
Note that the code takes a file name on the command line, defaulting to your "urls.txt". The "rt" open mode is not a POSIX or standard C mode; it will likely work, but "r" is sufficient to open a text file on all systems ("rb" to open a binary file works on all systems too, and is POSIX and standard C compliant). It reports which child process is processing each file listed. It reports the status (success or failure) of each child; it's own exit status is only success if all the children were successful.
You could probably control the verboseness from the command line. You might also want to keep a record of which child was processing each file so that you could report on files successfully downloaded, rather than on the processes which the user doesn't care about, really. That complicates the processing since you need to make a copy of each URL as you read it.
Note that you do need to trim the newlines off the end of the string (URL) before passing it to wget.
This code now tested (after adding the newline amendment), and it produced two files. The screen display is a bit of a mess; that's because each copy of wget thinks it is the sole user:
80334: ftp://ftp.iana.org/tz/releases/tzcode2012f.tar.gz
80335: ftp://ftp.iana.org/tz/releases/tzdata2012f.tar.gz
--2012-09-23 19:19:44-- ftp://ftp.iana.org/tz/releases/tzcode2012f.tar.gz
=> “tzcode2012f.tar.gz”
Resolving ftp.iana.org... --2012-09-23 19:19:44-- ftp://ftp.iana.org/tz/releases/tzdata2012f.tar.gz
=> “tzdata2012f.tar.gz”
Resolving ftp.iana.org... 192.0.32.8192.0.32.8, , 2620:0:2d0:200::82620:0:2d0:200::8
Connecting to ftp.iana.org|192.0.32.8|:21... Connecting to ftp.iana.org|192.0.32.8|:21... connected.
Logging in as anonymous ... connected.
Logging in as anonymous ... Logged in!
==> SYST ... Logged in!
==> SYST ... done. ==> PWD ... done. ==> PWD ... done.
==> TYPE I ... done.
==> TYPE I ... done. ==> CWD (1) /tz/releases ... done. ==> CWD (1) /tz/releases ... done.
==> SIZE tzdata2012f.tar.gz ... done.
==> SIZE tzcode2012f.tar.gz ... 206404
==> PASV ... 135543
==> PASV ... done. ==> RETR tzdata2012f.tar.gz ... done. ==> RETR tzcode2012f.tar.gz ... done.
Length: 206404 (202K) (unauthoritative)
0% [ ] 0 --.-K/s done.
Length: 135543 (132K) (unauthoritative)
100%[==============================================================================>] 135,543 72.7K/s in 1.8s
100%[==============================================================================>] 206,404 81.4K/s in 2.5s
2012-09-23 19:19:48 (72.7 KB/s) - “tzcode2012f.tar.gz” saved [135543]
Child 80334 OK
2012-09-23 19:19:48 (81.4 KB/s) - “tzdata2012f.tar.gz” saved [206404]
Child 80335 OK