From 4bcc8b64762e434498903fdbdd60fcbccc580f09 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Thu, 23 Jul 2026 08:59:42 +0200 Subject: [PATCH] Preserve Queue entries across spool failures --- contrib/testing/README.md | 14 ++ contrib/testing/queue-failure-safety-check.sh | 139 +++++++++++++ docs/queue.md | 6 + src/agents/queue/CMakeLists.txt | 10 + src/agents/queue/queue.c | 185 +++++++++++++----- src/agents/queue/spool-file.c | 128 ++++++++++++ src/agents/queue/spool-file.h | 38 ++++ src/agents/queue/tests/spool-file-test.c | 104 ++++++++++ 8 files changed, 576 insertions(+), 48 deletions(-) create mode 100755 contrib/testing/queue-failure-safety-check.sh create mode 100644 src/agents/queue/spool-file.c create mode 100644 src/agents/queue/spool-file.h create mode 100644 src/agents/queue/tests/spool-file-test.c diff --git a/contrib/testing/README.md b/contrib/testing/README.md index e7387ce..5088ece 100644 --- a/contrib/testing/README.md +++ b/contrib/testing/README.md @@ -94,6 +94,20 @@ reserve was subtracted without 32-bit truncation: ./contrib/testing/queue-disk-space-check.sh ``` +The Queue failure-safety check retains one isolated message while it first +raises the operative reserve above the filesystem's available bytes and then +makes the spool unreadable. It verifies rejection/defer responses, restores +the configuration and permissions through traps, and compares the retained +message byte-for-byte after both recoveries: + +```sh +./contrib/testing/queue-failure-safety-check.sh +``` + +It requires passwordless permission for `bongo-admin __config-read` and +`__config-replace`, Bongo service restarts, and `stat`/`chmod` on the +dedicated test spool. Run it only on a disposable test installation. + The Debian source probes and package-build commands used by BLD-14/BLD-15 are consolidated in `contrib/debian/build-trixie-bundle.sh`; they are not maintained as duplicate container-only snippets here. diff --git a/contrib/testing/queue-failure-safety-check.sh b/contrib/testing/queue-failure-safety-check.sh new file mode 100755 index 0000000..d98a3ba --- /dev/null +++ b/contrib/testing/queue-failure-safety-check.sh @@ -0,0 +1,139 @@ +#!/bin/sh +# +# Verify that a simulated full Queue filesystem rejects new submissions and +# that an unreadable spool defers access without changing an existing message. + +set -eu + +queue_tool=${BONGO_QUEUE_TOOL:-/usr/bin/bongo-queuetool} +admin_tool=${BONGO_ADMIN_TOOL:-/usr/bin/bongo-admin} +service=${BONGO_SERVICE:-bongo.service} +spool=${BONGO_QUEUE_SPOOL:-/var/lib/bongo/spool} +run_as=${BONGO_TEST_RUN_AS:-bongo} +as_root=${BONGO_TEST_AS_ROOT:-sudo -n} + +if [ "$(id -un)" = "$run_as" ]; then + as_bongo= +else + as_bongo="sudo -n -u $run_as" +fi + +work=$(mktemp -d "${TMPDIR:-/tmp}/bongo-stq04.XXXXXX") +chmod 0755 "$work" +queue_id= +spool_mode= +config_changed=0 + +restore() +{ + status=$? + if [ -n "$spool_mode" ]; then + $as_root /usr/bin/chmod "$spool_mode" "$spool" >/dev/null 2>&1 || true + fi + if [ "$config_changed" -eq 1 ]; then + $as_root "$admin_tool" __config-replace queue \ + <"$work/queue.original" >/dev/null 2>&1 || true + $as_root /usr/bin/systemctl restart "$service" >/dev/null 2>&1 || true + fi + if [ -n "$queue_id" ]; then + $as_bongo "$queue_tool" delete "$queue_id" >/dev/null 2>&1 || true + fi + rm -rf "$work" + exit "$status" +} +trap restore EXIT HUP INT TERM + +printf '%s\r\n' \ + 'From: STQ-04 ' \ + 'To: stqqueue@bongo.test' \ + 'Subject: STQ-04 retained queue fixture' \ + 'Date: Thu, 23 Jul 2026 12:34:56 +0200' \ + 'Message-ID: ' \ + '' \ + 'This exact message must survive both simulated failures.' \ + >"$work/message.eml" +chmod 0644 "$work/message.eml" + +$as_root "$admin_tool" __config-read queue >"$work/queue.original" +queue_id=$($as_bongo "$queue_tool" hold-local "$work/message.eml") +$as_bongo "$queue_tool" message "$queue_id" >"$work/before.eml" +before_hash=$(sha256sum "$work/before.eml" | awk '{ print $1 }') + +set -- $($as_bongo "$queue_tool" space details) +free_before=$1 +free_after=$2 +block=$5 +python3 - "$work/queue.original" "$work/queue.full" \ + "$free_before" "$free_after" "$block" <<'PY' +import json +import sys + +source, destination = sys.argv[1:3] +free_before, free_after, block = map(int, sys.argv[3:]) +with open(source, "r", encoding="utf-8") as stream: + config = json.load(stream) +config["minimumfreespace"] = max(free_before, free_after) + block + 1 +with open(destination, "w", encoding="utf-8") as stream: + json.dump(config, stream, indent=2, sort_keys=True) + stream.write("\n") +PY + +$as_root "$admin_tool" __config-replace queue <"$work/queue.full" +config_changed=1 +$as_root /usr/bin/systemctl restart "$service" + +reported=$($as_bongo "$queue_tool" space) +if [ "$reported" -ne 0 ]; then + echo "STQ-04: simulated full spool reported $reported usable bytes" >&2 + exit 1 +fi +if $as_bongo "$queue_tool" hold-local "$work/message.eml" \ + >"$work/full.out" 2>"$work/full.err"; then + echo "STQ-04: Queue accepted a message below its free-space reserve" >&2 + exit 1 +fi +if ! grep -F 'Free disk space too low' "$work/full.err" >/dev/null; then + echo "STQ-04: full-spool rejection did not report the expected error" >&2 + sed -n '1,20p' "$work/full.err" >&2 + exit 1 +fi +$as_bongo "$queue_tool" message "$queue_id" >"$work/after-full.eml" + +$as_root "$admin_tool" __config-replace queue <"$work/queue.original" +config_changed=0 +$as_root /usr/bin/systemctl restart "$service" + +spool_mode=$($as_root /usr/bin/stat -c %a "$spool") +$as_root /usr/bin/chmod 000 "$spool" +if $as_bongo "$queue_tool" message "$queue_id" \ + >"$work/unreadable.out" 2>"$work/unreadable.err"; then + echo "STQ-04: Queue read a deliberately unreadable spool" >&2 + exit 1 +fi +if ! grep -F "Can't read queue data" "$work/unreadable.err" >/dev/null; then + echo "STQ-04: unreadable spool did not report the expected error" >&2 + sed -n '1,20p' "$work/unreadable.err" >&2 + exit 1 +fi +$as_root /usr/bin/chmod "$spool_mode" "$spool" +spool_mode= +$as_bongo "$queue_tool" message "$queue_id" >"$work/after-unreadable.eml" + +if ! cmp -s "$work/before.eml" "$work/after-full.eml" || + ! cmp -s "$work/before.eml" "$work/after-unreadable.eml"; then + echo "STQ-04: retained Queue message changed during a failure" >&2 + exit 1 +fi +after_hash=$(sha256sum "$work/after-unreadable.eml" | awk '{ print $1 }') +if [ "$before_hash" != "$after_hash" ]; then + echo "STQ-04: retained Queue message digest changed" >&2 + exit 1 +fi +if [ "$($as_root /usr/bin/systemctl is-active "$service")" != active ]; then + echo "STQ-04: Bongo service is not active after recovery" >&2 + exit 1 +fi + +printf 'STQ-04 PASS queue=%s sha256=%s full-reported=%s spool-mode=%s\n' \ + "$queue_id" "$after_hash" "$reported" \ + "$($as_root /usr/bin/stat -c %a "$spool")" diff --git a/docs/queue.md b/docs/queue.md index 6693304..ce8c42a 100644 --- a/docs/queue.md +++ b/docs/queue.md @@ -38,6 +38,12 @@ for the configured retry path. Likewise, a per-recipient LMTP temporary error must preserve only that recipient for retry and must not redeliver recipients which already returned a final success. +Queue control-file rewrites use a same-directory work file and an atomic +rename. The original is retained unless every buffered write and close +succeeds. A failed submission flush or final rename is rejected and its +partial entry discarded, so the submitting SMTP peer remains responsible for +retrying the message instead of receiving a false success. + ## Administration and shutdown Use `bongo-queuetool` to inspect and perform documented queue operations; do diff --git a/src/agents/queue/CMakeLists.txt b/src/agents/queue/CMakeLists.txt index 6685e23..cf37180 100644 --- a/src/agents/queue/CMakeLists.txt +++ b/src/agents/queue/CMakeLists.txt @@ -7,6 +7,7 @@ add_executable(bongoqueue queued.c mime.c queue.c + spool-file.c store-delivery-format.c ) @@ -56,4 +57,13 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR} ) add_test(NAME queue-store-delivery-format COMMAND queue-store-delivery-format-test) + + add_executable(queue-spool-file-test + tests/spool-file-test.c + spool-file.c + ) + target_include_directories(queue-spool-file-test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME queue-spool-file COMMAND queue-spool-file-test) endif() diff --git a/src/agents/queue/queue.c b/src/agents/queue/queue.c index 9de3d26..751daab 100644 --- a/src/agents/queue/queue.c +++ b/src/agents/queue/queue.c @@ -32,6 +32,7 @@ #include "messages.h" #include "dsn-format.h" #include "envelope-format.h" +#include "spool-file.h" #include "store-delivery-format.h" static BOOL @@ -133,10 +134,17 @@ int fclose_check(FILE **fh, int line) { int ret; - ret = fclose(*fh); + FILE *stream; + + if (fh == NULL || *fh == NULL) { + errno = EINVAL; + return -1; + } + stream = *fh; + *fh = NULL; + ret = fclose(stream); if (ret == 0) { - // LogFailureF("File close: handle %ld", *fh); - *fh = NULL; + // LogFailureF("File close: handle %ld", stream); } else { LogFailureF("File close failed on line %d: %d", line, errno); } @@ -951,12 +959,15 @@ StartOver: MemFree(qEnvelope); - FCLOSE_CHECK(newFH); - - UNLINK_CHECK(path); - QueueFormat(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); - RENAME_CHECK(path2, path); + if (BongoQueueFileCommit(&newFH, path2, path) != 0) { + Log(LOG_WARNING, + "Could not atomically replace queue control file " + "'%s': %s; original retained", + path, strerror(errno)); + ProcessQueueEntryCleanUp(entryID, report); + return(TRUE); + } break; } @@ -3069,41 +3080,53 @@ CommandQaddq(void *param) return(ConnWrite(client->conn, MSG1000ENTRYMADE, sizeof(MSG1000ENTRYMADE) - 1)); } -int -CommandQabrt(void *param) +static void +DiscardClientEntry(QueueClient *client) { - int ccode; - QueueClient *client = (QueueClient *)param; - if (client->entry.control) { FCLOSE_CHECK(client->entry.control); - client->entry.control = NULL; - - QueueFormat(client->path,"%s/c%07lx.in", Conf.spoolPath, client->entry.id); - UNLINK_CHECK(client->path); } - + if (client->entry.id) { + QueueFormat(client->path,"%s/c%07lx.in", Conf.spoolPath, client->entry.id); + if (unlink(client->path) != 0 && errno != ENOENT) { + LogFailureF("Unable to discard queue control file %s: %d", + client->path, errno); + } + } + if (client->entry.data) { FCLOSE_CHECK(client->entry.data); - client->entry.data = NULL; - + } + if (client->entry.id) { QueueFormat(client->path,"%s/d%07lx.msg",Conf.spoolPath, client->entry.id); - UNLINK_CHECK(client->path); + if (unlink(client->path) != 0 && errno != ENOENT) { + LogFailureF("Unable to discard queue data file %s: %d", + client->path, errno); + } } if (client->entry.work) { FCLOSE_CHECK(client->entry.work); - client->entry.work = NULL; - - if (client->entry.workQueue[0] != '\0') { - QueueFormat(client->path,"%s/w%s.%s",Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); - UNLINK_CHECK(client->path); + } + if (client->entry.workQueue[0] != '\0') { + QueueFormat(client->path,"%s/w%s.%s",Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + if (unlink(client->path) != 0 && errno != ENOENT) { + LogFailureF("Unable to discard queue work file %s: %d", + client->path, errno); } } - + client->entry.id = 0; - - ccode = ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1); - return(ccode); + client->entry.target = 0; + client->entry.workQueue[0] = '\0'; +} + +int +CommandQabrt(void *param) +{ + QueueClient *client = (QueueClient *)param; + + DiscardClientEntry(client); + return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1)); } int @@ -3375,7 +3398,15 @@ CommandQcrea(void *param) QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id); FOPEN_CHECK(client->entry.control, client->path, "wb"); if (client->entry.control) { - fprintf(client->entry.control, QUEUES_DATE"%" PRIdMAX "\r\n", (intmax_t)time(NULL)); + if (BongoQueueFilePrintf( + client->entry.control, QUEUES_DATE"%" PRIdMAX "\r\n", + (intmax_t)time(NULL)) != 0 || + BongoQueueFileFlush(client->entry.control) != 0) { + FCLOSE_CHECK(client->entry.control); + unlink(client->path); + return(ConnWrite(client->conn, MSG5221SPACELOW, + sizeof(MSG5221SPACELOW) - 1)); + } } else { return(ConnWrite(client->conn, MSG5221SPACELOW, sizeof(MSG5221SPACELOW) - 1)); } @@ -4184,18 +4215,34 @@ CommandQrun(void *param) /* QRUN[ -] */ if (*ptr == '\0') { if (client->entry.control && client->entry.data && client->entry.id) { - FCLOSE_CHECK(client->entry.data); - client->entry.data = NULL; + int dataStatus = BongoQueueFileFinish(&client->entry.data); + int controlStatus = + BongoQueueFileFinish(&client->entry.control); - FCLOSE_CHECK(client->entry.control); - client->entry.control = NULL; + if (dataStatus != 0 || controlStatus != 0) { + Log(LOG_ERROR, + "Could not flush queue entry %07lx: %s; " + "submission rejected", + client->entry.id, strerror(errno)); + DiscardClientEntry(client); + return(ConnWrite(client->conn, MSG5221SPACELOW, + sizeof(MSG5221SPACELOW) - 1)); + } } else { return(ConnWrite(client->conn, MSG4000CANTUNLOCKENTRY, sizeof(MSG4000CANTUNLOCKENTRY) - 1)); } QueueFormat(client->path,"%s/c%07lx.in",Conf.spoolPath, client->entry.id); QueueFormat(path, "%s/c%07lx.%03ld", Conf.spoolPath, client->entry.id, client->entry.target); - RENAME_CHECK(client->path, path); + if (rename(client->path, path) != 0) { + Log(LOG_ERROR, + "Could not commit queue entry %07lx: %s; " + "submission rejected", + client->entry.id, strerror(errno)); + DiscardClientEntry(client); + return(ConnWrite(client->conn, MSG5221SPACELOW, + sizeof(MSG5221SPACELOW) - 1)); + } XplSafeIncrement(Queue.queuedLocal); @@ -4571,7 +4618,13 @@ CommandQstorCal(void *param) /* QSTOR CAL [ ][ ] */ if ((*ptr++ == ' ') && (!isspace(*ptr))) { - fprintf(client->entry.control, QUEUES_CALENDAR_LOCAL"%s\r\n", ptr); + if (BongoQueueFilePrintf( + client->entry.control, QUEUES_CALENDAR_LOCAL"%s\r\n", + ptr) != 0) { + DiscardClientEntry(client); + return(ConnWrite(client->conn, MSG5221SPACELOW, + sizeof(MSG5221SPACELOW) - 1)); + } return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1)); } @@ -4594,7 +4647,12 @@ CommandQstorFlags(void *param) /* QSTOR FLAGS */ if ((*ptr++ == ' ') && (!isspace(*ptr))) { - fprintf(client->entry.control, QUEUES_FLAGS"%s\r\n", ptr); + if (BongoQueueFilePrintf( + client->entry.control, QUEUES_FLAGS"%s\r\n", ptr) != 0) { + DiscardClientEntry(client); + return(ConnWrite(client->conn, MSG5221SPACELOW, + sizeof(MSG5221SPACELOW) - 1)); + } return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1)); } @@ -4616,7 +4674,12 @@ CommandQstorFrom(void *param) /* QSTOR FROM */ if ((*ptr++ == ' ') && (!isspace(*ptr))) { - fprintf(client->entry.control, QUEUES_FROM"%s\r\n", ptr); + if (BongoQueueFilePrintf( + client->entry.control, QUEUES_FROM"%s\r\n", ptr) != 0) { + DiscardClientEntry(client); + return(ConnWrite(client->conn, MSG5221SPACELOW, + sizeof(MSG5221SPACELOW) - 1)); + } return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1)); } @@ -4639,7 +4702,13 @@ CommandQstorLocal(void *param) /* QSTOR LOCAL */ if ((*ptr++ == ' ') && (!isspace(*ptr))) { - fprintf(client->entry.control, QUEUES_RECIP_LOCAL"%s\r\n", ptr); + if (BongoQueueFilePrintf( + client->entry.control, QUEUES_RECIP_LOCAL"%s\r\n", + ptr) != 0) { + DiscardClientEntry(client); + return(ConnWrite(client->conn, MSG5221SPACELOW, + sizeof(MSG5221SPACELOW) - 1)); + } return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1)); } @@ -4678,15 +4747,19 @@ CommandQstorMessage(void *param) } MsgGetRFC822Date(-1, 0, TimeBuf); - fprintf(client->entry.data, - "Received: from %s (%d.%d.%d.%d) by %s\r\n\twith NMAP (bongoqueue Agent); %s\r\n", - BongoGlobals.hostname, /* FIXME: this should eventually be the remote hsotname of the agent */ + if (BongoQueueFilePrintf( + client->entry.data, + "Received: from %s (%d.%d.%d.%d) by %s\r\n" + "\twith NMAP (bongoqueue Agent); %s\r\n", + BongoGlobals.hostname, client->conn->socketAddress.sin_addr.s_net, client->conn->socketAddress.sin_addr.s_host, client->conn->socketAddress.sin_addr.s_lh, client->conn->socketAddress.sin_addr.s_impno, - BongoGlobals.hostname, - TimeBuf); + BongoGlobals.hostname, TimeBuf) != 0) { + DiscardClientEntry(client); + return -1; + } /* fprintf(client->entry.data, "Received: from %d.%d.%d.%d [%d.%d.%d.%d] by %s\r\n\twith NMAP (%s); %s\r\n", @@ -4710,8 +4783,14 @@ CommandQstorMessage(void *param) ccode = ConnReadToFileUntilEOS(client->conn, client->entry.data); } - if (ccode != -1) { + if (ccode != -1 && BongoQueueFileFlush(client->entry.data) == 0) { ccode = ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1); + } else { + DiscardClientEntry(client); + if (ccode != -1) { + ccode = ConnWrite(client->conn, MSG5221SPACELOW, + sizeof(MSG5221SPACELOW) - 1); + } } return(ccode); @@ -4732,7 +4811,11 @@ CommandQstorRaw(void *param) /* QSTOR RAW */ if ((*ptr++ == ' ') && (!isspace(*ptr))) { - fprintf(client->entry.control,"%s\r\n", ptr); + if (BongoQueueFilePrintf(client->entry.control, "%s\r\n", ptr) != 0) { + DiscardClientEntry(client); + return(ConnWrite(client->conn, MSG5221SPACELOW, + sizeof(MSG5221SPACELOW) - 1)); + } return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1)); } @@ -4755,7 +4838,13 @@ CommandQstorTo(void *param) /* QSTOR TO */ if ((*ptr++ == ' ') && (!isspace(*ptr))) { - fprintf(client->entry.control,QUEUES_RECIP_REMOTE"%s\r\n", ptr); + if (BongoQueueFilePrintf( + client->entry.control, QUEUES_RECIP_REMOTE"%s\r\n", + ptr) != 0) { + DiscardClientEntry(client); + return(ConnWrite(client->conn, MSG5221SPACELOW, + sizeof(MSG5221SPACELOW) - 1)); + } return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1)); } diff --git a/src/agents/queue/spool-file.c b/src/agents/queue/spool-file.c new file mode 100644 index 0000000..c02f27f --- /dev/null +++ b/src/agents/queue/spool-file.c @@ -0,0 +1,128 @@ +/**************************************************************************** + * + * Copyright (c) 2001 Novell, Inc. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public License + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you + * may find current contact information at www.novell.com. + * + ****************************************************************************/ + +#include +#include +#include +#include + +#include "spool-file.h" + +int +BongoQueueFileFlush(FILE *stream) +{ + if (stream == NULL) { + errno = EINVAL; + return -1; + } + if (ferror(stream) || fflush(stream) != 0 || ferror(stream)) { + if (errno == 0) errno = EIO; + return -1; + } + return 0; +} + +int +BongoQueueFileFinish(FILE **stream) +{ + FILE *openStream; + int savedError = 0; + + if (stream == NULL || *stream == NULL) { + errno = EINVAL; + return -1; + } + openStream = *stream; + *stream = NULL; + if (BongoQueueFileFlush(openStream) != 0) savedError = errno; + if (fclose(openStream) != 0 && savedError == 0) savedError = errno; + if (savedError != 0) { + errno = savedError; + return -1; + } + return 0; +} + +int +BongoQueueFileCommit(FILE **stream, const char *workPath, + const char *destinationPath) +{ + int savedError; + + if (stream == NULL || *stream == NULL || workPath == NULL || + destinationPath == NULL) { + errno = EINVAL; + return -1; + } + if (BongoQueueFileFinish(stream) != 0) { + savedError = errno; + unlink(workPath); + errno = savedError; + return -1; + } + if (rename(workPath, destinationPath) != 0) { + savedError = errno; + unlink(workPath); + errno = savedError; + return -1; + } + return 0; +} + +int +BongoQueueFileWrite(FILE *stream, const void *data, size_t length) +{ + if (stream == NULL || (data == NULL && length != 0U)) { + errno = EINVAL; + return -1; + } + if (length != 0U && fwrite(data, 1U, length, stream) != length) { + if (errno == 0) errno = EIO; + return -1; + } + return 0; +} + +int +BongoQueueFileVPrintf(FILE *stream, const char *format, va_list arguments) +{ + if (stream == NULL || format == NULL) { + errno = EINVAL; + return -1; + } + if (vfprintf(stream, format, arguments) < 0 || ferror(stream)) { + if (errno == 0) errno = EIO; + return -1; + } + return 0; +} + +int +BongoQueueFilePrintf(FILE *stream, const char *format, ...) +{ + va_list arguments; + int result; + + va_start(arguments, format); + result = BongoQueueFileVPrintf(stream, format, arguments); + va_end(arguments); + return result; +} diff --git a/src/agents/queue/spool-file.h b/src/agents/queue/spool-file.h new file mode 100644 index 0000000..5a39fb7 --- /dev/null +++ b/src/agents/queue/spool-file.h @@ -0,0 +1,38 @@ +/**************************************************************************** + * + * Copyright (c) 2001 Novell, Inc. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public License + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you + * may find current contact information at www.novell.com. + * + ****************************************************************************/ + +#ifndef BONGO_QUEUE_SPOOL_FILE_H +#define BONGO_QUEUE_SPOOL_FILE_H + +#include +#include +#include + +int BongoQueueFileFlush(FILE *stream); +int BongoQueueFileFinish(FILE **stream); +int BongoQueueFileCommit(FILE **stream, const char *workPath, + const char *destinationPath); +int BongoQueueFileWrite(FILE *stream, const void *data, size_t length); +int BongoQueueFileVPrintf(FILE *stream, const char *format, + va_list arguments); +int BongoQueueFilePrintf(FILE *stream, const char *format, ...); + +#endif diff --git a/src/agents/queue/tests/spool-file-test.c b/src/agents/queue/tests/spool-file-test.c new file mode 100644 index 0000000..2062ced --- /dev/null +++ b/src/agents/queue/tests/spool-file-test.c @@ -0,0 +1,104 @@ +/**************************************************************************** + * + * Copyright (c) 2001 Novell, Inc. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public License + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you + * may find current contact information at www.novell.com. + * + ****************************************************************************/ + +#include +#include +#include +#include + +#include "spool-file.h" + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "check failed at line %d: %s\n", \ + __LINE__, #condition); \ + return 1; \ + } \ + } while (0) + +static int +ReadText(const char *path, char *buffer, size_t size) +{ + FILE *stream = fopen(path, "rb"); + size_t length; + + if (stream == NULL) return -1; + length = fread(buffer, 1U, size - 1U, stream); + buffer[length] = '\0'; + return fclose(stream); +} + +int +main(void) +{ + char directory[] = "/tmp/bongo-queue-spool-test.XXXXXX"; + char destination[256]; + char missingDestination[256]; + char work[256]; + char buffer[32]; + FILE *stream; + + CHECK(mkdtemp(directory) != NULL); + snprintf(destination, sizeof(destination), "%s/control", directory); + snprintf(work, sizeof(work), "%s/work", directory); + snprintf(missingDestination, sizeof(missingDestination), + "%s/missing/control", directory); + + stream = fopen(destination, "wb"); + CHECK(stream != NULL); + CHECK(BongoQueueFileWrite(stream, "original", 8U) == 0); + CHECK(BongoQueueFileFinish(&stream) == 0); + CHECK(stream == NULL); + + stream = fopen(work, "wb"); + CHECK(stream != NULL); + CHECK(BongoQueueFilePrintf(stream, "%s", "replacement") == 0); + CHECK(BongoQueueFileCommit(&stream, work, destination) == 0); + CHECK(stream == NULL); + CHECK(ReadText(destination, buffer, sizeof(buffer)) == 0); + CHECK(strcmp(buffer, "replacement") == 0); + + stream = fopen(work, "wb"); + CHECK(stream != NULL); + CHECK(BongoQueueFileWrite(stream, "discard", 7U) == 0); + CHECK(BongoQueueFileCommit( + &stream, work, missingDestination) != 0); + CHECK(access(work, F_OK) != 0); + CHECK(ReadText(destination, buffer, sizeof(buffer)) == 0); + CHECK(strcmp(buffer, "replacement") == 0); + + if (access("/dev/full", W_OK) == 0) { + CHECK(symlink("/dev/full", work) == 0); + stream = fopen(work, "wb"); + CHECK(stream != NULL); + CHECK(BongoQueueFilePrintf(stream, "%s", "must fail") == 0); + CHECK(BongoQueueFileCommit(&stream, work, destination) != 0); + CHECK(stream == NULL); + CHECK(access(work, F_OK) != 0); + CHECK(ReadText(destination, buffer, sizeof(buffer)) == 0); + CHECK(strcmp(buffer, "replacement") == 0); + } + + CHECK(unlink(destination) == 0); + CHECK(rmdir(directory) == 0); + return 0; +}