mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
Replace the readlink() branch with readlinkat() and normalize parameters: use AT_FDCWD with filename when relname is NULL. This removes duplicate code paths while preserving behavior, including the growth loop and size limits. readlinkat() is a superset of readlink(). Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com> Signed-off-by: Jan Polensky <japo@linux.ibm.com> Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
1861 lines
44 KiB
C
1861 lines
44 KiB
C
/*
|
|
* dump2tar - tool to dump files and command output into a tar archive
|
|
*
|
|
* Main dump logic
|
|
*
|
|
* Copyright IBM Corp. 2016, 2017
|
|
*
|
|
* s390-tools is free software; you can redistribute it and/or modify
|
|
* it under the terms of the MIT license. See LICENSE for details.
|
|
*/
|
|
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <fnmatch.h>
|
|
#include <getopt.h>
|
|
#include <pthread.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
#ifdef HAVE_ZLIB
|
|
#include <zlib.h>
|
|
#endif /* HAVE_ZLIB */
|
|
|
|
#include "buffer.h"
|
|
#include "dref.h"
|
|
#include "dump.h"
|
|
#include "global.h"
|
|
#include "misc.h"
|
|
#include "tar.h"
|
|
|
|
/* Default input file read size (bytes) */
|
|
#define DEFAULT_READ_CHUNK_SIZE (512 * 1024)
|
|
#define DEFAULT_MAX_BUFFER_SIZE (2 * 1024 * 1024)
|
|
|
|
#define _SET_ABORTED(task) _set_aborted((task), __func__, __LINE__)
|
|
#define SET_ABORTED(task) set_aborted((task), __func__, __LINE__)
|
|
|
|
#define read_error(task, filename, fmt, ...) \
|
|
do { \
|
|
if (!(task)->opts->ignore_failed_read) \
|
|
SET_ABORTED((task)); \
|
|
_mwarn(true, "%s: " fmt, (filename), ##__VA_ARGS__); \
|
|
} while (0)
|
|
|
|
#define write_error(task, fmt, ...) \
|
|
do { \
|
|
SET_ABORTED((task)); \
|
|
_mwarn(true, "%s: " fmt, (task)->opts->output_file, \
|
|
##__VA_ARGS__); \
|
|
} while (0)
|
|
|
|
#define tverb(fmt, ...) \
|
|
do { \
|
|
if (task->opts->verbose) \
|
|
verb((fmt), ##__VA_ARGS__); \
|
|
} while (0)
|
|
|
|
|
|
/* Jobs representing a file or command output to add */
|
|
struct job {
|
|
struct job *next_job;
|
|
enum job_type {
|
|
JOB_INIT, /* Initialization work */
|
|
JOB_FILE, /* Add a regular file */
|
|
JOB_LINK, /* Add a symbolic link */
|
|
JOB_DIR, /* Add a directory */
|
|
JOB_CMD, /* Add command output */
|
|
} type;
|
|
enum job_status {
|
|
JOB_QUEUED, /* Transient: Job processing has not started */
|
|
JOB_IN_PROGRESS,/* Transient: Job processing has started */
|
|
JOB_EXCLUDED, /* Final: File was excluded */
|
|
JOB_FAILED, /* Final: Data could not be obtained */
|
|
JOB_DONE, /* Final: All data was obtained */
|
|
JOB_PARTIAL, /* Final: Only some data was obtained */
|
|
} status;
|
|
char *outname;
|
|
char *inname;
|
|
char *relname;
|
|
struct stat stat;
|
|
bool timed;
|
|
struct timespec deadline;
|
|
struct dref *dref;
|
|
int cmd_status;
|
|
struct buffer *content;
|
|
};
|
|
|
|
/* Run-time statistics */
|
|
struct stats {
|
|
unsigned long num_done;
|
|
unsigned long num_excluded;
|
|
unsigned long num_failed;
|
|
unsigned long num_partial;
|
|
};
|
|
|
|
/* Information specific to a single dump task */
|
|
struct task {
|
|
/* Input */
|
|
struct dump_opts *opts;
|
|
|
|
/* State */
|
|
|
|
/* mutex serializes access to global data */
|
|
pthread_mutex_t mutex;
|
|
pthread_cond_t worker_cond;
|
|
pthread_cond_t cond;
|
|
unsigned long num_jobs_active;
|
|
struct job *jobs_head;
|
|
struct job *jobs_tail;
|
|
bool aborted;
|
|
|
|
/* output_mutex serializes access to output file */
|
|
pthread_mutex_t output_mutex;
|
|
int output_fd;
|
|
size_t output_written;
|
|
#ifdef HAVE_ZLIB
|
|
gzFile output_gzfd;
|
|
#endif /* HAVE_ZLIB */
|
|
unsigned long output_num_files;
|
|
|
|
/* No protection needed (only accessed in single-threaded mode) */
|
|
struct stats stats;
|
|
struct timespec start_ts;
|
|
};
|
|
|
|
/* Per thread management data */
|
|
struct per_thread {
|
|
long num;
|
|
pthread_t thread;
|
|
bool running;
|
|
bool timed_out;
|
|
struct stats stats;
|
|
struct job *job;
|
|
struct buffer buffer;
|
|
struct task *task;
|
|
};
|
|
|
|
static const struct {
|
|
mode_t mode;
|
|
char c;
|
|
} exclude_types[NUM_EXCLUDE_TYPES] = {
|
|
{ S_IFREG, 'f' },
|
|
{ S_IFDIR, 'd' },
|
|
{ S_IFCHR, 'c' },
|
|
{ S_IFBLK, 'b' },
|
|
{ S_IFIFO, 'p' },
|
|
{ S_IFLNK, 'l' },
|
|
{ S_IFSOCK, 's' },
|
|
};
|
|
|
|
/* Lock main mutex */
|
|
static void main_lock(struct task *task)
|
|
{
|
|
if (!global_threaded)
|
|
return;
|
|
DBG("main lock");
|
|
pthread_mutex_lock(&task->mutex);
|
|
}
|
|
|
|
/* Unlock main mutex */
|
|
static void main_unlock(struct task *task)
|
|
{
|
|
if (!global_threaded)
|
|
return;
|
|
DBG("main unlock");
|
|
pthread_mutex_unlock(&task->mutex);
|
|
}
|
|
|
|
/* Lock output mutex */
|
|
static void output_lock(struct task *task)
|
|
{
|
|
if (!global_threaded)
|
|
return;
|
|
pthread_mutex_lock(&task->output_mutex);
|
|
}
|
|
|
|
/* Unlock output mutex */
|
|
static void output_unlock(struct task *task)
|
|
{
|
|
if (!global_threaded)
|
|
return;
|
|
pthread_mutex_unlock(&task->output_mutex);
|
|
}
|
|
|
|
/* Wake up all waiting workers */
|
|
static void _worker_wakeup_all(struct task *task)
|
|
{
|
|
if (!global_threaded)
|
|
return;
|
|
DBG("waking up all worker threads");
|
|
pthread_cond_broadcast(&task->worker_cond);
|
|
}
|
|
|
|
/* Wake up one waiting worker */
|
|
static void _worker_wakeup_one(struct task *task)
|
|
{
|
|
if (!global_threaded)
|
|
return;
|
|
DBG("waking up one worker thread");
|
|
pthread_cond_signal(&task->worker_cond);
|
|
}
|
|
|
|
/* Wait for a signal to a worker */
|
|
static int _worker_wait(struct task *task)
|
|
{
|
|
int rc;
|
|
|
|
DBG("waiting for signal to worker");
|
|
rc = pthread_cond_wait(&task->worker_cond, &task->mutex);
|
|
DBG("waiting for signal to worker done (rc=%d)", rc);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Wake up main thread */
|
|
static void _main_wakeup(struct task *task)
|
|
{
|
|
if (!global_threaded)
|
|
return;
|
|
DBG("waking up main thread");
|
|
pthread_cond_broadcast(&task->cond);
|
|
}
|
|
|
|
/* Wait for a signal to the main thread */
|
|
static int _main_wait(struct task *task)
|
|
{
|
|
int rc;
|
|
|
|
DBG("waiting for status change");
|
|
rc = pthread_cond_wait(&task->cond, &task->mutex);
|
|
DBG("waiting for status change done (rc=%d)", rc);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Wait for a signal to the main thread. Abort waiting after @deadline */
|
|
static int _main_wait_timed(struct task *task, struct timespec *deadline)
|
|
{
|
|
int rc;
|
|
|
|
DBG("timed waiting for status change");
|
|
rc = pthread_cond_timedwait(&task->cond, &task->mutex, deadline);
|
|
DBG("timed waiting for status change done (rc=%d)", rc);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Allow thread to be canceled */
|
|
static void cancel_enable(void)
|
|
{
|
|
if (!global_threaded)
|
|
return;
|
|
if (pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL) != 0)
|
|
mwarn("pthread_setcancelstate");
|
|
}
|
|
|
|
/* Prevent thread from being canceled */
|
|
static void cancel_disable(void)
|
|
{
|
|
if (!global_threaded)
|
|
return;
|
|
if (pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL) != 0)
|
|
mwarn("pthread_setcancelstate");
|
|
}
|
|
|
|
/* Abort processing and inform all threads to shutdown. Must be called with
|
|
* task->mutex locked */
|
|
static void _set_aborted(struct task *task, const char *func, unsigned int line)
|
|
{
|
|
DBG("set aborted at %s:%u", func, line);
|
|
task->aborted = true;
|
|
_worker_wakeup_all(task);
|
|
_main_wakeup(task);
|
|
}
|
|
|
|
/* Abort processing and inform all threads to shutdown */
|
|
static void set_aborted(struct task *task, const char *func, unsigned int line)
|
|
{
|
|
main_lock(task);
|
|
_set_aborted(task, func, line);
|
|
main_unlock(task);
|
|
}
|
|
|
|
/* Check if abort processing has been initiated */
|
|
static bool is_aborted(struct task *task)
|
|
{
|
|
bool result;
|
|
|
|
main_lock(task);
|
|
result = task->aborted;
|
|
main_unlock(task);
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Release resources associated with @job */
|
|
static void free_job(struct task *task, struct job *job)
|
|
{
|
|
DBG("free job %p (%s)", job, job->inname);
|
|
if (!job)
|
|
return;
|
|
free(job->inname);
|
|
free(job->outname);
|
|
free(job->relname);
|
|
dref_put(job->dref);
|
|
free(job);
|
|
}
|
|
|
|
/* Check if file type specified by mode @m was marked as excluded */
|
|
static bool is_type_excluded(struct dump_opts *opts, mode_t m)
|
|
{
|
|
int i;
|
|
|
|
m &= S_IFMT;
|
|
for (i = 0; i < NUM_EXCLUDE_TYPES; i++) {
|
|
if (exclude_types[i].mode == m)
|
|
return opts->exclude_type[i];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Replace all '/' characters in @filename with '_' */
|
|
static void escape_filename(char *filename)
|
|
{
|
|
for (; *filename; filename++) {
|
|
if (*filename == '/')
|
|
*filename = '_';
|
|
}
|
|
}
|
|
|
|
/* Determine filename in archive from original filename @inname and
|
|
* requested new filename @outname and depending on @type. */
|
|
static void set_outname(char **result_ptr, const char *outname,
|
|
const char *inname, enum job_type type)
|
|
{
|
|
const char *prefix = "", *name, *suffix;
|
|
char *result, *end;
|
|
size_t olen = outname ? strlen(outname) : 0, plen, nlen;
|
|
|
|
if (olen == 0) {
|
|
/* No output name specified: outname = inname */
|
|
name = inname;
|
|
} else if (outname[olen - 1] == '/') {
|
|
/* Output name is a directory: outname = outname/inname */
|
|
prefix = outname;
|
|
name = inname;
|
|
} else {
|
|
/* Output name is a filename: outname = inname */
|
|
name = outname;
|
|
}
|
|
|
|
if (type == JOB_DIR)
|
|
suffix = "/";
|
|
else
|
|
suffix = "";
|
|
|
|
plen = strlen(prefix);
|
|
nlen = strlen(name);
|
|
|
|
result = mmalloc(plen + nlen + strlen(suffix) + /* NUL */ 1);
|
|
|
|
/* Add prefix */
|
|
strcpy(result, prefix);
|
|
|
|
/* Add name */
|
|
end = result + plen;
|
|
strcpy(end, name);
|
|
if (type == JOB_CMD)
|
|
escape_filename(end);
|
|
|
|
/* Add suffix */
|
|
end = end + nlen;
|
|
strcpy(end, suffix);
|
|
|
|
remove_double_slashes(result);
|
|
|
|
*result_ptr = result;
|
|
}
|
|
|
|
static void sanitize_dirname(char **name_ptr)
|
|
{
|
|
char *name;
|
|
|
|
name = mmalloc(strlen(*name_ptr) + /* Slash */ 1 + /* NUL */ 1);
|
|
strcpy(name, *name_ptr);
|
|
remove_double_slashes(name);
|
|
chomp(name, "/");
|
|
strcat(name, "/");
|
|
free(*name_ptr);
|
|
*name_ptr = name;
|
|
}
|
|
|
|
/* Allocate and initialize a new job representation to add an entry according
|
|
* to the specified parameters. @relname and @dref are used for opening files
|
|
* more efficiently using *at() functions if specified. @is_cmd specifies if
|
|
* the specified inname is a command line. */
|
|
static struct job *create_job(struct task *task, const char *inname,
|
|
const char *outname, bool is_cmd,
|
|
const char *relname, struct dref *dref,
|
|
struct stats *stats)
|
|
{
|
|
struct job *job = mmalloc(sizeof(struct job));
|
|
int rc;
|
|
|
|
DBG("create job inname=%s outname=%s is_cmd=%d relname=%s dref=%p",
|
|
inname, outname, is_cmd, relname, dref);
|
|
|
|
job->status = JOB_QUEUED;
|
|
|
|
if (!inname) {
|
|
job->type = JOB_INIT;
|
|
return job;
|
|
}
|
|
|
|
job->inname = mstrdup(inname);
|
|
|
|
if (is_cmd) {
|
|
/* Special case - read from command output */
|
|
job->type = JOB_CMD;
|
|
set_dummy_stat(&job->stat);
|
|
goto out;
|
|
}
|
|
|
|
if (!relname && strcmp(job->inname, "-") == 0) {
|
|
/* Special case - read from standard input */
|
|
job->type = JOB_FILE;
|
|
set_dummy_stat(&job->stat);
|
|
goto out;
|
|
}
|
|
|
|
rc = stat_file(task->opts->dereference, job->inname, relname, dref,
|
|
&job->stat);
|
|
|
|
if (rc < 0) {
|
|
read_error(task, job->inname, "Cannot stat file");
|
|
free_job(task, job);
|
|
stats->num_failed++;
|
|
return NULL;
|
|
}
|
|
|
|
if (is_type_excluded(task->opts, job->stat.st_mode)) {
|
|
free_job(task, job);
|
|
stats->num_excluded++;
|
|
return NULL;
|
|
}
|
|
|
|
if (S_ISLNK(job->stat.st_mode)) {
|
|
job->type = JOB_LINK;
|
|
} else if (S_ISDIR(job->stat.st_mode)) {
|
|
job->type = JOB_DIR;
|
|
sanitize_dirname(&job->inname);
|
|
|
|
/* No need to keep parent directory open */
|
|
relname = NULL;
|
|
dref = NULL;
|
|
} else {
|
|
job->type = JOB_FILE;
|
|
}
|
|
|
|
if (relname)
|
|
job->relname = mstrdup(relname);
|
|
job->dref = dref_get(dref);
|
|
|
|
out:
|
|
set_outname(&job->outname, outname, inname, job->type);
|
|
|
|
return job;
|
|
}
|
|
|
|
void job_print(struct job *job)
|
|
{
|
|
printf("DEBUG: job_print at %p\n", job);
|
|
printf("DEBUG: next_job=%p\n", job->next_job);
|
|
printf("DEBUG: type=%d\n", job->type);
|
|
printf("DEBUG: status==%d\n", job->status);
|
|
printf("DEBUG: outname=%s\n", job->outname);
|
|
printf("DEBUG: inname=%s\n", job->inname);
|
|
printf("DEBUG: relname=%s\n", job->relname);
|
|
printf("DEBUG: timed=%d\n", job->timed);
|
|
printf("DEBUG: dref=%p\n", job->dref);
|
|
printf("DEBUG: cmd_status=%d\n", job->cmd_status);
|
|
printf("DEBUG: content=%p\n", job->content);
|
|
}
|
|
|
|
/* Return the number of bytes written to the output file */
|
|
static size_t get_output_size(struct task *task)
|
|
{
|
|
#ifdef HAVE_ZLIB
|
|
if (task->opts->gzip) {
|
|
gzflush(task->output_gzfd, Z_SYNC_FLUSH);
|
|
return gztell(task->output_gzfd);
|
|
}
|
|
#endif /* HAVE_ZLIB */
|
|
return task->output_written;
|
|
}
|
|
|
|
/* Write @len bytes at address @ptr to the output file */
|
|
static int write_output(struct task *task, const char *ptr, size_t len)
|
|
{
|
|
size_t todo = len;
|
|
ssize_t w;
|
|
|
|
#ifdef HAVE_ZLIB
|
|
if (task->opts->gzip) {
|
|
if (gzwrite(task->output_gzfd, ptr, len) == 0)
|
|
goto err_write;
|
|
task->output_written += len;
|
|
|
|
return EXIT_OK;
|
|
}
|
|
#endif /* HAVE_ZLIB */
|
|
|
|
while (todo > 0) {
|
|
w = write(task->output_fd, ptr, todo);
|
|
if (w < 0)
|
|
goto err_write;
|
|
todo -= w;
|
|
ptr += w;
|
|
}
|
|
task->output_written += len;
|
|
|
|
return EXIT_OK;
|
|
|
|
err_write:
|
|
write_error(task, "Cannot write output");
|
|
|
|
return EXIT_RUNTIME;
|
|
}
|
|
|
|
/* Write an end-of-file marker to the output file */
|
|
static void write_eof(struct task *task)
|
|
{
|
|
char zeroes[TAR_BLOCKSIZE];
|
|
|
|
memset(zeroes, 0, sizeof(zeroes));
|
|
write_output(task, zeroes, TAR_BLOCKSIZE);
|
|
write_output(task, zeroes, TAR_BLOCKSIZE);
|
|
}
|
|
|
|
/* Callback for writing out chunks of job data */
|
|
static int _write_job_data_cb(void *data, void *addr, size_t len)
|
|
{
|
|
struct task *task = data;
|
|
|
|
return write_output(task, addr, len);
|
|
}
|
|
|
|
/* Write tar entry for a file containing the exit status of the process that
|
|
* ran command job @job */
|
|
static int write_job_status_file(struct task *task, struct job *job)
|
|
{
|
|
char *name, *content;
|
|
size_t len;
|
|
struct stat st;
|
|
int rc, status = job->cmd_status, exitstatus = -1, termsig = -1,
|
|
waitpid_errno = -1;
|
|
|
|
name = masprintf("%s.cmdstatus", job->outname);
|
|
if (status < 0)
|
|
waitpid_errno = -status;
|
|
else if (WIFEXITED(status))
|
|
exitstatus = WEXITSTATUS(status);
|
|
else if (WIFSIGNALED(status))
|
|
termsig = WTERMSIG(status);
|
|
|
|
content = masprintf("EXITSTATUS=%d\n"
|
|
"TERMSIG=%d\n"
|
|
"WAITPID_ERRNO=%d\n", exitstatus, termsig,
|
|
waitpid_errno);
|
|
|
|
len = strlen(content);
|
|
set_dummy_stat(&st);
|
|
rc = tar_emit_file_from_data(name, NULL, len, &st, TYPE_REGULAR,
|
|
content, _write_job_data_cb, task);
|
|
free(name);
|
|
free(content);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Write tar entry for data in @job to output. Must be called with output_lock
|
|
* held. */
|
|
static void _write_job_data(struct task *task, struct job *job)
|
|
{
|
|
struct buffer *buffer = job->content;
|
|
|
|
switch (job->status) {
|
|
case JOB_DONE:
|
|
case JOB_PARTIAL:
|
|
break;
|
|
case JOB_FAILED:
|
|
/* Create empty entries for failed reads */
|
|
if (task->opts->ignore_failed_read)
|
|
break;
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
switch (job->type) {
|
|
case JOB_CMD:
|
|
tar_emit_file_from_buffer(job->outname, NULL, buffer->total,
|
|
&job->stat, TYPE_REGULAR, buffer,
|
|
_write_job_data_cb, task);
|
|
task->output_num_files++;
|
|
if (task->opts->add_cmd_status) {
|
|
write_job_status_file(task, job);
|
|
task->output_num_files++;
|
|
}
|
|
break;
|
|
case JOB_FILE:
|
|
tar_emit_file_from_buffer(job->outname, NULL, buffer->total,
|
|
&job->stat, TYPE_REGULAR, buffer,
|
|
_write_job_data_cb, task);
|
|
task->output_num_files++;
|
|
break;
|
|
case JOB_LINK:
|
|
tar_emit_file_from_buffer(job->outname, buffer->addr, 0,
|
|
&job->stat, TYPE_LINK, NULL,
|
|
_write_job_data_cb, task);
|
|
task->output_num_files++;
|
|
break;
|
|
case JOB_DIR:
|
|
tar_emit_file_from_buffer(job->outname, NULL, 0, &job->stat,
|
|
TYPE_DIR, NULL, _write_job_data_cb,
|
|
task);
|
|
task->output_num_files++;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (task->opts->max_size > 0 &&
|
|
get_output_size(task) > task->opts->max_size) {
|
|
mwarnx("Archive size exceeds maximum of %ld bytes - aborting",
|
|
task->opts->max_size);
|
|
SET_ABORTED(task);
|
|
}
|
|
}
|
|
|
|
/* Read the contents of the symbolic link at @filename. On success, the
|
|
* contents is returned in @buffer and the return value is %EXIT_OK.
|
|
* If @relname is non-null it points to the name of the file relative
|
|
* to its parent directory for which @dirfd is an open file handle. */
|
|
static int read_symlink(struct task *task, const char *filename,
|
|
const char *relname, int dirfd, struct buffer *buffer)
|
|
{
|
|
ssize_t actual = 0;
|
|
size_t currlen = buffer->size ? buffer->size :
|
|
task->opts->read_chunk_size;
|
|
int rc = EXIT_OK;
|
|
|
|
/* If @relname is NULL, use @filename with AT_FDCWD. */
|
|
if (!relname) {
|
|
relname = filename;
|
|
dirfd = AT_FDCWD;
|
|
}
|
|
|
|
while (!is_aborted(task)) {
|
|
buffer_make_room(buffer, currlen, false,
|
|
task->opts->max_buffer_size);
|
|
|
|
cancel_enable();
|
|
actual = readlinkat(dirfd, relname, buffer->addr, buffer->size);
|
|
cancel_disable();
|
|
|
|
if (actual == -1) {
|
|
read_error(task, filename, "Cannot read link");
|
|
rc = EXIT_RUNTIME;
|
|
/* Reset actual counter to get an empty buffer */
|
|
actual = 0;
|
|
break;
|
|
}
|
|
|
|
/* Ensure that content doesn't exceed --file-max-size limit */
|
|
if (task->opts->file_max_size > 0 &&
|
|
(size_t) actual > task->opts->file_max_size) {
|
|
actual = task->opts->file_max_size;/* Don't count NUL */
|
|
mwarnx("%s: Warning: Data exceeds maximum size of %ld "
|
|
"bytes - truncating", filename,
|
|
task->opts->file_max_size);
|
|
break;
|
|
}
|
|
|
|
if ((size_t) actual < buffer->size)
|
|
break;
|
|
|
|
currlen += task->opts->read_chunk_size;
|
|
}
|
|
|
|
if (rc == EXIT_OK && is_aborted(task))
|
|
rc = EXIT_RUNTIME;
|
|
|
|
buffer->addr[actual] = 0;
|
|
buffer->total = actual + 1;
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Read data from the file descriptor @fd until an end-of-file condition is
|
|
* encountered. On success, *@done bytes in @buffer contain the read data
|
|
* and the return value is %EXIT_OK. */
|
|
static int read_fd(struct task *task, const char *name, int fd,
|
|
struct buffer *buffer)
|
|
{
|
|
ssize_t rc = 0;
|
|
size_t c = buffer->size ? buffer->size : task->opts->read_chunk_size;
|
|
|
|
while (!is_aborted(task)) {
|
|
cancel_enable();
|
|
rc = buffer_read_fd(buffer, fd, c, true,
|
|
task->opts->max_buffer_size);
|
|
cancel_disable();
|
|
|
|
if (rc <= 0)
|
|
break;
|
|
|
|
/* Ensure that content doesn't exceed --file-max-size limit */
|
|
if (task->opts->file_max_size > 0 &&
|
|
buffer->total >= task->opts->file_max_size) {
|
|
buffer_truncate(buffer, task->opts->file_max_size);
|
|
rc = 0;
|
|
mwarnx("%s: Warning: Data exceeds maximum size of %ld "
|
|
"bytes - truncating", name,
|
|
task->opts->file_max_size);
|
|
break;
|
|
}
|
|
|
|
c = buffer->size - buffer->off;
|
|
if (c > 0) {
|
|
/* Read to memory */
|
|
} else if (buffer->size + task->opts->read_chunk_size <
|
|
task->opts->max_buffer_size) {
|
|
/* Enlarge memory buffer */
|
|
c = task->opts->read_chunk_size;
|
|
} else {
|
|
/* Use full memory buffer size */
|
|
c = task->opts->max_buffer_size;
|
|
}
|
|
}
|
|
|
|
if (is_aborted(task) || rc != 0)
|
|
return EXIT_RUNTIME;
|
|
|
|
return EXIT_OK;
|
|
}
|
|
|
|
/* Read data from the file at @filename until an end-of-file condition is
|
|
* encountered. On success, @buffer contains the data read and the return
|
|
* value is %EXIT_OK. If @relname is non-null it points to the name of the
|
|
* file relative to its parent directory for which @dirfd is an open file
|
|
* handle. */
|
|
static int read_regular(struct task *task, const char *filename,
|
|
const char *relname, int dirfd, struct buffer *buffer)
|
|
{
|
|
int fd, rc = EXIT_OK;
|
|
bool need_close = true;
|
|
|
|
/* Opening a named pipe can block when peer is not ready */
|
|
cancel_enable();
|
|
if (strcmp(filename, "-") == 0) {
|
|
fd = STDIN_FILENO;
|
|
need_close = false;
|
|
filename = "Standard input";
|
|
} else if (relname)
|
|
fd = openat(dirfd, relname, O_RDONLY);
|
|
else
|
|
fd = open(filename, O_RDONLY);
|
|
cancel_disable();
|
|
|
|
if (fd < 0) {
|
|
read_error(task, filename, "Cannot open file");
|
|
return EXIT_RUNTIME;
|
|
}
|
|
|
|
rc = read_fd(task, filename, fd, buffer);
|
|
if (rc) {
|
|
if (is_aborted(task))
|
|
mwarnx("%s: Read aborted", filename);
|
|
else
|
|
read_error(task, filename, "Cannot read file");
|
|
}
|
|
|
|
if (need_close)
|
|
close(fd);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Read the output of command @cmd until an end-of-file condition is
|
|
* encountered. On success, @buffer contain the output and the return value
|
|
* is %EXIT_OK. When not %NULL, use @status_ptr to store the resulting process
|
|
* status. */
|
|
static int read_cmd_output(struct task *task, char *cmd, struct buffer *buffer,
|
|
int *status_ptr)
|
|
{
|
|
int fd, rc = EXIT_RUNTIME;
|
|
pid_t pid;
|
|
|
|
fd = cmd_open(cmd, &pid);
|
|
if (fd < 0) {
|
|
read_error(task, cmd, "Cannot run command");
|
|
return rc;
|
|
}
|
|
|
|
if (read_fd(task, cmd, fd, buffer)) {
|
|
if (is_aborted(task))
|
|
mwarnx("%s: Command aborted", cmd);
|
|
else
|
|
read_error(task, cmd, "Cannot read command output");
|
|
} else
|
|
rc = EXIT_OK;
|
|
|
|
cmd_close(fd, pid, status_ptr);
|
|
|
|
return rc;
|
|
|
|
}
|
|
|
|
/* Check the exclude patterns in @task->opts->exclude for a match of @filename.
|
|
* If found, return the matching pattern string, otherwise return %NULL. */
|
|
static const char *get_exclude_match(struct task *task, const char *filename)
|
|
{
|
|
unsigned int i;
|
|
int mode = FNM_PERIOD | FNM_NOESCAPE;
|
|
|
|
for (i = 0; i < task->opts->exclude.num; i++) {
|
|
if (fnmatch(task->opts->exclude.str[i], filename, mode) == 0)
|
|
return task->opts->exclude.str[i];
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Add the specified @job to the start of the job queue */
|
|
static void _queue_job_head(struct task *task, struct job *job)
|
|
{
|
|
DBG("queue job type=%d inname=%s at head", job->type, job->inname);
|
|
job->next_job = task->jobs_head;
|
|
task->jobs_head = job;
|
|
if (!task->jobs_tail)
|
|
task->jobs_tail = job;
|
|
}
|
|
|
|
/* Add the specified @job to the end of the job queue */
|
|
static void _queue_job_tail(struct task *task, struct job *job)
|
|
{
|
|
DBG("queue job type=%d inname=%s at tail", job->type, job->inname);
|
|
if (task->jobs_tail)
|
|
task->jobs_tail->next_job = job;
|
|
else
|
|
task->jobs_head = job;
|
|
task->jobs_tail = job;
|
|
}
|
|
|
|
/* Add the specified @job to the job queue and trigger processing.
|
|
* If @head is %true, the new job is inserted at the start of the job queue,
|
|
* otherwise at the end. */
|
|
static void queue_job(struct task *task, struct job *job, bool head)
|
|
{
|
|
main_lock(task);
|
|
task->num_jobs_active++;
|
|
if (head)
|
|
_queue_job_head(task, job);
|
|
else
|
|
_queue_job_tail(task, job);
|
|
_worker_wakeup_one(task);
|
|
main_unlock(task);
|
|
}
|
|
|
|
/* Add the specified list of jobs starting with @first up to @last to the start
|
|
* of the job queue and trigger processing */
|
|
static void queue_jobs(struct task *task, struct job *first, struct job *last,
|
|
int num)
|
|
{
|
|
main_lock(task);
|
|
last->next_job = task->jobs_head;
|
|
task->jobs_head = first;
|
|
task->num_jobs_active += num;
|
|
_worker_wakeup_all(task);
|
|
main_unlock(task);
|
|
}
|
|
|
|
/* Remove the head of the job queue and return it to the caller */
|
|
static struct job *_dequeue_job(struct task *task)
|
|
{
|
|
struct job *job = NULL;
|
|
|
|
if (task->jobs_head) {
|
|
job = task->jobs_head;
|
|
task->jobs_head = job->next_job;
|
|
job->next_job = NULL;
|
|
if (job == task->jobs_tail)
|
|
task->jobs_tail = NULL;
|
|
DBG("dequeueing job type=%d inname=%s", job->type, job->inname);
|
|
job->status = JOB_IN_PROGRESS;
|
|
} else {
|
|
DBG("no job to dequeue");
|
|
}
|
|
|
|
return job;
|
|
}
|
|
|
|
/* Create and queue job for file at @filename */
|
|
static void queue_file(struct task *task, const char *inname,
|
|
const char *outname, bool is_cmd,
|
|
const char *relname, struct dref *dref,
|
|
struct stats *stats, bool head)
|
|
{
|
|
struct job *job;
|
|
|
|
job = create_job(task, inname, outname, is_cmd, relname, dref, stats);
|
|
if (job)
|
|
queue_job(task, job, head);
|
|
}
|
|
|
|
/* Queue initial job */
|
|
static void init_queue(struct task *task)
|
|
{
|
|
queue_file(task, NULL, NULL, false, NULL, NULL, NULL, true);
|
|
}
|
|
|
|
/* Create and queue jobs for all files found in @dirname */
|
|
static void queue_dir(struct task *task, const char *dirname,
|
|
const char *outname, struct stats *stats)
|
|
{
|
|
struct dirent *de;
|
|
char *inpath, *outpath;
|
|
struct dref *dref;
|
|
struct job *job, *first = NULL, *last = NULL;
|
|
int num = 0;
|
|
|
|
dref = dref_create(dirname);
|
|
if (!dref) {
|
|
read_error(task, dirname, "Cannot read directory");
|
|
return;
|
|
}
|
|
|
|
while ((de = readdir(dref->dd))) {
|
|
if (de->d_name[0] == '.') {
|
|
if (de->d_name[1] == 0)
|
|
continue;
|
|
if (de->d_name[1] == '.' && de->d_name[2] == 0)
|
|
continue;
|
|
}
|
|
DBG("next file %s", de->d_name);
|
|
inpath = masprintf("%s%s", dirname, de->d_name);
|
|
outpath = masprintf("%s%s", outname, de->d_name);
|
|
job = create_job(task, inpath, outpath, false, de->d_name, dref,
|
|
stats);
|
|
if (job) {
|
|
if (last) {
|
|
last->next_job = job;
|
|
last = job;
|
|
} else {
|
|
first = job;
|
|
last = job;
|
|
}
|
|
num++;
|
|
}
|
|
free(inpath);
|
|
free(outpath);
|
|
}
|
|
|
|
if (first)
|
|
queue_jobs(task, first, last, num);
|
|
|
|
dref_put(dref);
|
|
}
|
|
|
|
/* Create and queue jobs for all files specified on the command line */
|
|
static void queue_jobs_from_opts(struct task *task, struct stats *stats)
|
|
{
|
|
struct dump_opts *opts = task->opts;
|
|
unsigned int i;
|
|
|
|
/* Queue directly specified entries */
|
|
for (i = 0; i < opts->num_specs && !is_aborted(task); i++) {
|
|
queue_file(task, opts->specs[i].inname, opts->specs[i].outname,
|
|
opts->specs[i].is_cmd, NULL, NULL, stats, false);
|
|
}
|
|
}
|
|
|
|
/* Prepare output stream */
|
|
static int open_output(struct task *task)
|
|
{
|
|
bool to_stdout = !task->opts->output_file ||
|
|
strcmp(task->opts->output_file, "-") == 0;
|
|
int rc = EXIT_OK;
|
|
struct stat st;
|
|
|
|
if (to_stdout) {
|
|
set_stdout_data();
|
|
task->opts->output_file = "Standard output";
|
|
}
|
|
|
|
cancel_enable();
|
|
#ifdef HAVE_ZLIB
|
|
if (task->opts->gzip) {
|
|
if (to_stdout) {
|
|
task->output_gzfd =
|
|
gzdopen(STDOUT_FILENO,
|
|
task->opts->append ? "ab" : "wb");
|
|
} else {
|
|
task->output_gzfd =
|
|
gzopen(task->opts->output_file,
|
|
task->opts->append ? "ab" : "wb");
|
|
}
|
|
|
|
if (!task->output_gzfd)
|
|
rc = EXIT_RUNTIME;
|
|
goto out;
|
|
}
|
|
#endif /* HAVE_ZLIB */
|
|
|
|
if (to_stdout) {
|
|
task->output_fd = STDOUT_FILENO;
|
|
} else {
|
|
task->output_fd =
|
|
open(task->opts->output_file, O_WRONLY | O_CREAT |
|
|
(task->opts->append ? O_APPEND : 0), 0666);
|
|
}
|
|
|
|
if (task->output_fd < 0)
|
|
rc = EXIT_RUNTIME;
|
|
else if (!task->opts->append) {
|
|
if (fstat(task->output_fd, &st) == -1)
|
|
rc = EXIT_RUNTIME;
|
|
else if (S_ISREG(st.st_mode) &&
|
|
ftruncate(task->output_fd, 0) == -1)
|
|
rc = EXIT_RUNTIME;
|
|
}
|
|
|
|
#ifdef HAVE_ZLIB
|
|
out:
|
|
#endif /* HAVE_ZLIB */
|
|
cancel_disable();
|
|
|
|
if (rc != EXIT_OK) {
|
|
mwarn("%s: Cannot open output file", task->opts->output_file);
|
|
return rc;
|
|
}
|
|
|
|
return EXIT_OK;
|
|
}
|
|
|
|
/* Determine if the specified @job should be excluded from archiving */
|
|
static bool is_job_excluded(struct task *task, struct job *job)
|
|
{
|
|
const char *pat;
|
|
|
|
if (job->type == JOB_INIT || job->type == JOB_CMD)
|
|
return false;
|
|
|
|
pat = get_exclude_match(task, job->inname);
|
|
if (!pat)
|
|
return false;
|
|
|
|
tverb("Excluding '%s' due to exclude pattern '%s'\n", job->inname, pat);
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Perform all actions necessary to process @job and add resulting tar
|
|
* data buffers to the buffer list of @thread. */
|
|
static void process_job(struct per_thread *thread, struct job *job)
|
|
{
|
|
struct task *task = thread->task;
|
|
const char *relname = job->dref ? job->relname : NULL;
|
|
int dirfd = job->dref ? job->dref->dirfd : -1;
|
|
struct buffer *buffer = &thread->buffer;
|
|
enum job_status status = JOB_DONE;
|
|
|
|
DBG("processing job type=%d inname=%s", job->type, job->inname);
|
|
|
|
if (is_job_excluded(task, job)) {
|
|
status = JOB_EXCLUDED;
|
|
goto out;
|
|
}
|
|
|
|
switch (job->type) {
|
|
case JOB_INIT: /* Perform initial setup steps */
|
|
if (open_output(task)) {
|
|
SET_ABORTED(task);
|
|
status = JOB_FAILED;
|
|
goto out;
|
|
}
|
|
queue_jobs_from_opts(task, &thread->stats);
|
|
break;
|
|
case JOB_CMD: /* Capture command output */
|
|
tverb("Dumping command output '%s'\n", job->inname);
|
|
|
|
set_dummy_stat(&job->stat);
|
|
if (read_cmd_output(task, job->inname, buffer,
|
|
&job->cmd_status))
|
|
status = JOB_FAILED;
|
|
|
|
break;
|
|
case JOB_LINK: /* Read symbolic link */
|
|
tverb("Dumping link '%s'\n", job->inname);
|
|
|
|
if (read_symlink(task, job->inname, relname, dirfd, buffer))
|
|
status = JOB_FAILED;
|
|
|
|
break;
|
|
case JOB_DIR: /* Read directory contents */
|
|
tverb("Dumping directory '%s'\n", job->inname);
|
|
|
|
if (task->opts->recursive) {
|
|
queue_dir(task, job->inname, job->outname,
|
|
&thread->stats);
|
|
}
|
|
break;
|
|
case JOB_FILE: /* Read file contents */
|
|
tverb("Dumping file '%s'\n", job->inname);
|
|
|
|
if (read_regular(task, job->inname, relname, dirfd, buffer))
|
|
status = JOB_FAILED;
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
out:
|
|
job->status = status;
|
|
DBG("processing done status=%d", job->status);
|
|
}
|
|
|
|
/* Add @job results to statistics @stats */
|
|
static void account_stats(struct task *task, struct stats *stats,
|
|
struct job *job)
|
|
{
|
|
DBG("accounting job %s", job->inname);
|
|
|
|
if (job->type == JOB_INIT)
|
|
return;
|
|
|
|
switch (job->status) {
|
|
case JOB_DONE:
|
|
stats->num_done++;
|
|
if (job->type == JOB_CMD && task->opts->add_cmd_status)
|
|
stats->num_done++;
|
|
break;
|
|
case JOB_PARTIAL:
|
|
stats->num_done++;
|
|
stats->num_partial++;
|
|
if (job->type == JOB_CMD && task->opts->add_cmd_status)
|
|
stats->num_done++;
|
|
break;
|
|
case JOB_FAILED:
|
|
stats->num_failed++;
|
|
break;
|
|
case JOB_EXCLUDED:
|
|
stats->num_excluded++;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Add statistics @from to @to */
|
|
static void add_stats(struct stats *to, struct stats *from)
|
|
{
|
|
to->num_done += from->num_done;
|
|
to->num_partial += from->num_partial;
|
|
to->num_excluded += from->num_excluded;
|
|
to->num_failed += from->num_failed;
|
|
}
|
|
|
|
/* Release resources allocated to @thread */
|
|
static void cleanup_thread(struct per_thread *thread)
|
|
{
|
|
if (thread->job)
|
|
free_job(thread->task, thread->job);
|
|
buffer_free(&thread->buffer, false);
|
|
}
|
|
|
|
/* Register activate @job at @thread */
|
|
static void start_thread_job(struct per_thread *thread, struct job *job)
|
|
{
|
|
struct task *task = thread->task;
|
|
|
|
thread->job = job;
|
|
job->content = &thread->buffer;
|
|
if (task->opts->file_timeout > 0 && job->type != JOB_INIT) {
|
|
/* Set up per-job timeout */
|
|
set_timespec(&job->deadline, task->opts->file_timeout, 0);
|
|
job->timed = true;
|
|
|
|
/* Signal main thread to update deadline timeout */
|
|
_main_wakeup(task);
|
|
}
|
|
}
|
|
|
|
/* Unregister active @job at @thread */
|
|
static void stop_thread_job(struct per_thread *thread, struct job *job)
|
|
{
|
|
thread->job = NULL;
|
|
job->content = NULL;
|
|
buffer_reset(&thread->buffer);
|
|
}
|
|
|
|
/* Wait until a job is available in the job queue. When a job becomes
|
|
* available, dequeue and return it. Return %NULL if no more jobs are
|
|
* available, or if processing was aborted. Must be called with task->mutex
|
|
* locked. */
|
|
static struct job *_get_next_job(struct task *task)
|
|
{
|
|
struct job *job = NULL;
|
|
|
|
do {
|
|
DBG("checking for jobs");
|
|
if (task->aborted)
|
|
break;
|
|
job = _dequeue_job(task);
|
|
if (job)
|
|
break;
|
|
if (task->num_jobs_active == 0)
|
|
break;
|
|
DBG("found no jobs (%d active)", task->num_jobs_active);
|
|
} while (_worker_wait(task) == 0);
|
|
|
|
return job;
|
|
}
|
|
|
|
/* Unlock the mutex specified by @data */
|
|
static void cleanup_unlock(void *data)
|
|
{
|
|
pthread_mutex_t *mutex = data;
|
|
|
|
pthread_mutex_unlock(mutex);
|
|
}
|
|
|
|
/* Write entry for data in @job to output */
|
|
static void write_job_data(struct task *task, struct job *job)
|
|
{
|
|
DBG("write_job_data");
|
|
output_lock(task);
|
|
pthread_cleanup_push(cleanup_unlock, &task->output_mutex);
|
|
cancel_enable();
|
|
|
|
_write_job_data(task, job);
|
|
|
|
cancel_disable();
|
|
pthread_cleanup_pop(0);
|
|
output_unlock(task);
|
|
}
|
|
|
|
/* Perform second part of job processing for @job at @thread by writing the
|
|
* resulting tar file entry */
|
|
static void postprocess_job(struct per_thread *thread, struct job *job,
|
|
bool cancelable)
|
|
{
|
|
struct task *task = thread->task;
|
|
|
|
account_stats(task, &thread->stats, job);
|
|
if (cancelable)
|
|
write_job_data(task, job);
|
|
else
|
|
_write_job_data(task, job);
|
|
}
|
|
|
|
/* Mark @job as complete by releasing all associated resources. If this was
|
|
* the last active job inform main thread. Must be called with main_lock
|
|
* mutex held. */
|
|
static void _complete_job(struct task *task, struct job *job)
|
|
{
|
|
task->num_jobs_active--;
|
|
if (task->num_jobs_active == 0)
|
|
_main_wakeup(task);
|
|
free_job(task, job);
|
|
}
|
|
|
|
static void init_thread(struct per_thread *thread, struct task *task, long num)
|
|
{
|
|
memset(thread, 0, sizeof(struct per_thread));
|
|
thread->task = task;
|
|
thread->num = num;
|
|
}
|
|
|
|
/* Dequeue and process all jobs on the job queue */
|
|
static int process_queue(struct task *task)
|
|
{
|
|
struct job *job;
|
|
struct per_thread thread;
|
|
|
|
init_thread(&thread, task, 0);
|
|
|
|
while ((job = _dequeue_job(task)) && !is_aborted(task)) {
|
|
start_thread_job(&thread, job);
|
|
process_job(&thread, job);
|
|
postprocess_job(&thread, job, false);
|
|
stop_thread_job(&thread, job);
|
|
_complete_job(task, job);
|
|
}
|
|
|
|
task->stats = thread.stats;
|
|
cleanup_thread(&thread);
|
|
|
|
return EXIT_OK;
|
|
}
|
|
|
|
/* Return %true if @job is in a final state, %false otherwise */
|
|
static bool job_is_final(struct job *job)
|
|
{
|
|
switch (job->status) {
|
|
case JOB_DONE:
|
|
case JOB_PARTIAL:
|
|
case JOB_EXCLUDED:
|
|
case JOB_FAILED:
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Main thread function: process jobs on the job queue until all jobs
|
|
* are processed or processing was aborted. */
|
|
static void *worker_thread_main(void *d)
|
|
{
|
|
struct per_thread *thread = d;
|
|
struct task *task = thread->task;
|
|
struct job *job;
|
|
|
|
/* Allow cancel only at specific code points */
|
|
cancel_disable();
|
|
set_threadname("%*sworker %d", (thread->num + 1) * 2, "", thread->num);
|
|
|
|
/* Handle jobs left over from canceled thread */
|
|
job = thread->job;
|
|
if (job) {
|
|
DBG("handle aborted job %p", job);
|
|
|
|
postprocess_job(thread, job, true);
|
|
|
|
main_lock(task);
|
|
if (thread->timed_out)
|
|
goto out;
|
|
stop_thread_job(thread, job);
|
|
_complete_job(task, job);
|
|
main_unlock(task);
|
|
}
|
|
|
|
DBG("enter worker loop");
|
|
|
|
main_lock(task);
|
|
while ((job = _get_next_job(task))) {
|
|
start_thread_job(thread, job);
|
|
main_unlock(task);
|
|
|
|
process_job(thread, job);
|
|
postprocess_job(thread, job, true);
|
|
|
|
main_lock(task);
|
|
if (thread->timed_out)
|
|
goto out;
|
|
stop_thread_job(thread, job);
|
|
_complete_job(task, job);
|
|
}
|
|
|
|
out:
|
|
thread->running = false;
|
|
_main_wakeup(task);
|
|
main_unlock(task);
|
|
|
|
cancel_enable();
|
|
DBG("leave work loop");
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Start a worker thread associated with the specified @data. Return %EXIT_OK on
|
|
* success. */
|
|
static int start_worker_thread(struct per_thread *data)
|
|
{
|
|
int rc;
|
|
|
|
DBG("start thread");
|
|
global_threaded = true;
|
|
data->timed_out = false;
|
|
rc = pthread_create(&data->thread, NULL, &worker_thread_main, data);
|
|
if (rc) {
|
|
mwarnx("Cannot start thread: %s", strerror(rc));
|
|
return EXIT_RUNTIME;
|
|
}
|
|
data->running = true;
|
|
|
|
return EXIT_OK;
|
|
}
|
|
|
|
/* Perform timeout handling for thread associated with @data by canceling and
|
|
* restarting the corresponding thread. Must be called with task->mutex
|
|
* held. */
|
|
static void _timeout_thread(struct per_thread *data)
|
|
{
|
|
struct task *task = data->task;
|
|
struct job *job = data->job;
|
|
pthread_t thread = data->thread;
|
|
const char *op, *action;
|
|
|
|
if (!job) {
|
|
/* Timeout raced with job completion */
|
|
return;
|
|
}
|
|
if (job_is_final(job)) {
|
|
/* Job processing done, timeout does not apply */
|
|
return;
|
|
}
|
|
|
|
data->timed_out = true;
|
|
|
|
/* Allow thread to obtain main lock during cancel handling */
|
|
main_unlock(task);
|
|
DBG("cancel num=%d thread=%p", data->num, thread);
|
|
pthread_cancel(thread);
|
|
DBG("join num=%d thread=%p", data->num, thread);
|
|
|
|
pthread_join(thread, NULL);
|
|
main_lock(task);
|
|
|
|
DBG("join done");
|
|
|
|
if (job->type == JOB_CMD)
|
|
op = "Command";
|
|
else
|
|
op = "Read";
|
|
|
|
if (task->opts->ignore_failed_read)
|
|
action = "skipping";
|
|
else
|
|
action = "aborting";
|
|
|
|
if (!job->inname || !*job->inname)
|
|
job_print(job);
|
|
mwarnx("%s: %s%s timed out after %d second%s - %s", job->inname,
|
|
task->opts->ignore_failed_read ? "Warning: " : "", op,
|
|
task->opts->file_timeout,
|
|
task->opts->file_timeout > 1 ? "s" : "", action);
|
|
if (!task->opts->ignore_failed_read)
|
|
_SET_ABORTED(task);
|
|
|
|
/* Interrupted job will be handled by new thread - adjust status */
|
|
if (job->status == JOB_IN_PROGRESS)
|
|
job->status = JOB_PARTIAL;
|
|
else if (!job_is_final(job))
|
|
job->status = JOB_FAILED;
|
|
|
|
if (start_worker_thread(data))
|
|
_SET_ABORTED(task);
|
|
}
|
|
|
|
/* Return the number of currently running jobs */
|
|
static long num_jobs_running(struct task *task, struct per_thread *threads)
|
|
{
|
|
long i, num = 0;
|
|
|
|
for (i = 0; i < task->opts->jobs; i++) {
|
|
if (threads[i].running)
|
|
num++;
|
|
}
|
|
|
|
return num;
|
|
}
|
|
|
|
/* Wait until all jobs are done or timeout occurs */
|
|
static int wait_for_completion(struct task *task, struct per_thread *threads)
|
|
{
|
|
int rc = 0, earliest_timeout;
|
|
long i;
|
|
struct per_thread *earliest_thread;
|
|
struct timespec tool_deadline_ts, deadline_ts, *earliest_ts;
|
|
struct job *job;
|
|
|
|
/* Set tool deadline */
|
|
tool_deadline_ts = task->start_ts;
|
|
inc_timespec(&tool_deadline_ts, task->opts->timeout, 0);
|
|
|
|
main_lock(task);
|
|
while (!task->aborted && task->num_jobs_active > 0) {
|
|
/* Calculate nearest timeout */
|
|
earliest_timeout = 0;
|
|
earliest_ts = NULL;
|
|
earliest_thread = NULL;
|
|
|
|
if (task->opts->timeout > 0) {
|
|
earliest_timeout = task->opts->timeout;
|
|
earliest_ts = &tool_deadline_ts;
|
|
}
|
|
|
|
for (i = 0; i < task->opts->jobs; i++) {
|
|
job = threads[i].job;
|
|
if (!job || !job->timed)
|
|
continue;
|
|
if (task->opts->file_timeout == 0)
|
|
continue;
|
|
if (!earliest_ts ||
|
|
ts_before(&job->deadline, earliest_ts)) {
|
|
earliest_timeout = task->opts->file_timeout;
|
|
earliest_ts = &job->deadline;
|
|
earliest_thread = &threads[i];
|
|
}
|
|
}
|
|
|
|
/* Wait for status change or timeout */
|
|
if (earliest_ts) {
|
|
deadline_ts = *earliest_ts;
|
|
rc = _main_wait_timed(task, &deadline_ts);
|
|
} else {
|
|
rc = _main_wait(task);
|
|
}
|
|
|
|
if (rc == 0)
|
|
continue;
|
|
if (rc != ETIMEDOUT) {
|
|
mwarnx("Cannot wait for status change: %s",
|
|
strerror(rc));
|
|
_SET_ABORTED(task);
|
|
break;
|
|
}
|
|
|
|
/* Timeout handling */
|
|
if (earliest_thread) {
|
|
/* Per-file timeout, restart */
|
|
_timeout_thread(earliest_thread);
|
|
rc = 0;
|
|
} else {
|
|
/* Global timeout, abort */
|
|
mwarnx("Operation timed out after %d second%s - "
|
|
"aborting", earliest_timeout,
|
|
earliest_timeout > 1 ? "s" : "");
|
|
_SET_ABORTED(task);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (task->aborted)
|
|
DBG("aborted");
|
|
else
|
|
DBG("all work done");
|
|
_worker_wakeup_all(task);
|
|
|
|
/* Allow jobs to finish */
|
|
set_timespec(&deadline_ts, 0, NSEC_PER_SEC / 4);
|
|
while (!task->aborted && num_jobs_running(task, threads) > 0) {
|
|
DBG("waiting for %lu processes",
|
|
num_jobs_running(task, threads));
|
|
|
|
if (_main_wait_timed(task, &deadline_ts))
|
|
break;
|
|
}
|
|
|
|
main_unlock(task);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Finalize output stream */
|
|
static void close_output(struct task *task)
|
|
{
|
|
#ifdef HAVE_ZLIB
|
|
if (task->opts->gzip) {
|
|
gzclose(task->output_gzfd);
|
|
return;
|
|
}
|
|
#endif /* HAVE_ZLIB */
|
|
|
|
if (task->output_fd != STDOUT_FILENO)
|
|
close(task->output_fd);
|
|
}
|
|
|
|
/* Start multi-threaded processing of job queue */
|
|
static int process_queue_threaded(struct task *task)
|
|
{
|
|
struct per_thread *threads, *thread;
|
|
int rc;
|
|
long i;
|
|
|
|
tverb("Using %ld threads\n", task->opts->jobs);
|
|
threads = mcalloc(sizeof(struct per_thread), task->opts->jobs);
|
|
|
|
rc = 0;
|
|
for (i = 0; i < task->opts->jobs; i++) {
|
|
init_thread(&threads[i], task, i);
|
|
rc = start_worker_thread(&threads[i]);
|
|
if (rc)
|
|
break;
|
|
}
|
|
|
|
if (!rc)
|
|
wait_for_completion(task, threads);
|
|
|
|
DBG("thread cleanup");
|
|
for (i = 0; i < task->opts->jobs; i++) {
|
|
thread = &threads[i];
|
|
if (thread->running) {
|
|
DBG("cancel %p", thread->thread);
|
|
pthread_cancel(thread->thread);
|
|
}
|
|
DBG("join %p", thread->thread);
|
|
pthread_join(thread->thread, NULL);
|
|
add_stats(&task->stats, &thread->stats);
|
|
cleanup_thread(thread);
|
|
}
|
|
|
|
free(threads);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Abort any remaining queued jobs and account to @stats */
|
|
static void abort_queued_jobs(struct task *task)
|
|
{
|
|
struct job *job;
|
|
|
|
while ((job = _dequeue_job(task))) {
|
|
DBG("aborting job %s", job->inname);
|
|
task->stats.num_failed++;
|
|
job->status = JOB_FAILED;
|
|
_complete_job(task, job);
|
|
}
|
|
}
|
|
|
|
/* Print a summary line */
|
|
static void print_summary(struct task *task)
|
|
{
|
|
char msg[MSG_LEN];
|
|
size_t off = 0;
|
|
int rc;
|
|
struct stats *stats = &task->stats;
|
|
struct timespec end_ts;
|
|
int num_special;
|
|
unsigned long num_added;
|
|
|
|
if (task->opts->quiet)
|
|
return;
|
|
set_timespec(&end_ts, 0, 0);
|
|
|
|
num_special = 0;
|
|
num_special += stats->num_partial > 0 ? 1 : 0;
|
|
num_special += stats->num_excluded > 0 ? 1 : 0;
|
|
num_special += stats->num_failed > 0 ? 1 : 0;
|
|
|
|
num_added = stats->num_done;
|
|
if (task->opts->ignore_failed_read)
|
|
num_added += stats->num_partial + stats->num_failed;
|
|
|
|
rc = snprintf(&msg[off], MSG_LEN - off, "Dumped %lu entries ",
|
|
num_added);
|
|
HANDLE_RC(rc, MSG_LEN, off, out);
|
|
|
|
if (num_special > 0) {
|
|
rc = snprintf(&msg[off], MSG_LEN - off, "(");
|
|
HANDLE_RC(rc, MSG_LEN, off, out);
|
|
if (stats->num_partial > 0) {
|
|
rc = snprintf(&msg[off], MSG_LEN - off, "%lu partial",
|
|
stats->num_partial);
|
|
HANDLE_RC(rc, MSG_LEN, off, out);
|
|
if (--num_special > 0) {
|
|
rc = snprintf(&msg[off], MSG_LEN - off, ", ");
|
|
HANDLE_RC(rc, MSG_LEN, off, out);
|
|
}
|
|
}
|
|
if (stats->num_excluded > 0) {
|
|
rc = snprintf(&msg[off], MSG_LEN - off, "%lu excluded",
|
|
stats->num_excluded);
|
|
HANDLE_RC(rc, MSG_LEN, off, out);
|
|
if (--num_special > 0) {
|
|
rc = snprintf(&msg[off], MSG_LEN - off, ", ");
|
|
HANDLE_RC(rc, MSG_LEN, off, out);
|
|
}
|
|
}
|
|
if (stats->num_failed > 0) {
|
|
rc = snprintf(&msg[off], MSG_LEN - off, "%lu failed",
|
|
stats->num_failed);
|
|
HANDLE_RC(rc, MSG_LEN, off, out);
|
|
}
|
|
rc = snprintf(&msg[off], MSG_LEN - off, ") ");
|
|
HANDLE_RC(rc, MSG_LEN, off, out);
|
|
}
|
|
|
|
rc = snprintf(&msg[off], MSG_LEN - off, "in ");
|
|
HANDLE_RC(rc, MSG_LEN, off, out);
|
|
snprintf_duration(&msg[off], MSG_LEN - off, &task->start_ts, &end_ts);
|
|
|
|
out:
|
|
info("%s\n", msg);
|
|
}
|
|
|
|
static int init_task(struct task *task, struct dump_opts *opts)
|
|
{
|
|
pthread_condattr_t attr;
|
|
|
|
memset(task, 0, sizeof(struct task));
|
|
set_timespec(&task->start_ts, 0, 0);
|
|
task->opts = opts;
|
|
pthread_mutex_init(&task->mutex, NULL);
|
|
pthread_mutex_init(&task->output_mutex, NULL);
|
|
pthread_cond_init(&task->worker_cond, NULL);
|
|
|
|
pthread_condattr_init(&attr);
|
|
if (pthread_condattr_setclock(&attr, CLOCK_MONOTONIC) ||
|
|
pthread_cond_init(&task->cond, &attr)) {
|
|
mwarn("Could not adjust pthread clock");
|
|
return EXIT_RUNTIME;
|
|
}
|
|
|
|
return EXIT_OK;
|
|
}
|
|
|
|
struct dump_opts *dump_opts_new(void)
|
|
{
|
|
struct dump_opts *opts = mmalloc(sizeof(struct dump_opts));
|
|
|
|
opts->recursive = true;
|
|
opts->read_chunk_size = DEFAULT_READ_CHUNK_SIZE;
|
|
opts->max_buffer_size = DEFAULT_MAX_BUFFER_SIZE;
|
|
|
|
return opts;
|
|
}
|
|
|
|
void dump_opts_free(struct dump_opts *opts)
|
|
{
|
|
unsigned int i;
|
|
|
|
if (!opts)
|
|
return;
|
|
|
|
free_strarray(&opts->exclude);
|
|
for (i = 0; i < opts->num_specs; i++) {
|
|
free(opts->specs[i].inname);
|
|
free(opts->specs[i].outname);
|
|
}
|
|
free(opts->specs);
|
|
free(opts);
|
|
}
|
|
|
|
void dump_opts_print(struct dump_opts *opts)
|
|
{
|
|
unsigned int i;
|
|
|
|
printf("DEBUG: dump_opts at %p\n", opts);
|
|
if (!opts)
|
|
return;
|
|
printf("DEBUG: add_cmd_status=%d\n", opts->add_cmd_status);
|
|
printf("DEBUG: append=%d\n", opts->append);
|
|
printf("DEBUG: dereference=%d\n", opts->dereference);
|
|
for (i = 0; i < NUM_EXCLUDE_TYPES; i++)
|
|
printf("DEBUG: exclude_type[%d]=%d\n", i,
|
|
opts->exclude_type[i]);
|
|
printf("DEBUG: gzip=%d\n", opts->gzip);
|
|
printf("DEBUG: ignore_failed_read=%d\n", opts->ignore_failed_read);
|
|
printf("DEBUG: no_eof=%d\n", opts->no_eof);
|
|
printf("DEBUG: quiet=%d\n", opts->quiet);
|
|
printf("DEBUG: recursive=%d\n", opts->recursive);
|
|
printf("DEBUG: threaded=%d\n", opts->threaded);
|
|
printf("DEBUG: verbose=%d\n", opts->verbose);
|
|
printf("DEBUG: output_file=%s\n", opts->output_file);
|
|
printf("DEBUG: file_timeout=%d\n", opts->file_timeout);
|
|
printf("DEBUG: timeout=%d\n", opts->timeout);
|
|
printf("DEBUG: jobs=%ld\n", opts->jobs);
|
|
printf("DEBUG: jobs_per_cpu=%ld\n", opts->jobs_per_cpu);
|
|
printf("DEBUG: file_max_size=%zu\n", opts->file_max_size);
|
|
printf("DEBUG: max_buffer_size=%zu\n", opts->max_buffer_size);
|
|
printf("DEBUG: max_size=%zu\n", opts->max_size);
|
|
printf("DEBUG: read_chunk_size=%zu\n", opts->read_chunk_size);
|
|
for (i = 0; i < opts->exclude.num; i++)
|
|
printf("DEBUG: exclude[%d]=%s\n", i, opts->exclude.str[i]);
|
|
for (i = 0; i < opts->num_specs; i++) {
|
|
printf("DEBUG: specs[%d]:\n", i);
|
|
printf("DEBUG: inname=%s\n", opts->specs[i].inname);
|
|
printf("DEBUG: outname=%s\n", opts->specs[i].outname);
|
|
printf("DEBUG: is_cmd=%d\n", opts->specs[i].is_cmd);
|
|
}
|
|
}
|
|
|
|
/* Mark file type associated with character @c as excluded */
|
|
int dump_opts_set_type_excluded(struct dump_opts *opts, char c)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < NUM_EXCLUDE_TYPES; i++) {
|
|
if (exclude_types[i].c == c) {
|
|
opts->exclude_type[i] = true;
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* Add entry specification defined by @iname, @outname and @op to @opts. */
|
|
void dump_opts_add_spec(struct dump_opts *opts, char *inname, char *outname,
|
|
bool is_cmd)
|
|
{
|
|
unsigned int i = opts->num_specs;
|
|
|
|
opts->specs = mrealloc(opts->specs, (i + 1) * sizeof(struct dump_spec));
|
|
opts->specs[i].inname = mstrdup(inname);
|
|
if (outname)
|
|
opts->specs[i].outname = mstrdup(outname);
|
|
else
|
|
opts->specs[i].outname = NULL;
|
|
opts->specs[i].is_cmd = is_cmd;
|
|
opts->num_specs++;
|
|
}
|
|
|
|
int dump_to_tar(struct dump_opts *opts)
|
|
{
|
|
struct task task;
|
|
int rc;
|
|
long num_cpus;
|
|
|
|
if (opts->jobs_per_cpu > 0) {
|
|
num_cpus = sysconf(_SC_NPROCESSORS_ONLN);
|
|
if (num_cpus < 1) {
|
|
mwarn("Cannot determine number of CPUs - assuming 1 "
|
|
"CPU");
|
|
num_cpus = 1;
|
|
}
|
|
opts->jobs = num_cpus;
|
|
}
|
|
|
|
if (opts->jobs == 0 && (opts->timeout > 0 || opts->file_timeout > 0)) {
|
|
/* Separate thread needed to implement timeout via cancel */
|
|
opts->jobs = 1;
|
|
}
|
|
|
|
rc = init_task(&task, opts);
|
|
if (rc)
|
|
return rc;
|
|
|
|
/* Queue initial job */
|
|
init_queue(&task);
|
|
|
|
/* Process queue */
|
|
if (opts->jobs > 0)
|
|
rc = process_queue_threaded(&task);
|
|
else
|
|
rc = process_queue(&task);
|
|
abort_queued_jobs(&task);
|
|
|
|
if (task.output_num_files > 0 && !opts->no_eof)
|
|
write_eof(&task);
|
|
|
|
print_summary(&task);
|
|
|
|
close_output(&task);
|
|
|
|
if (rc == 0 && task.aborted)
|
|
rc = EXIT_RUNTIME;
|
|
|
|
return rc;
|
|
}
|