mirror of
https://github.com/itsPLK/ps5-pkg-manager.git
synced 2026-10-22 11:53:08 -05:00
* ws: direct-install streaming over WebSocket (RAM-only, no disk spool) Browser pushes .pkg bytes over ws://:8846 while the PS5 installer pulls ranges over :8845 concurrently through a demand-paged 1MB-segment ring (64 slots, seg 0 pinned). Install can start at header_ready, mid-upload. Backend (src/ws_stream.c, src/ws_upload.c): - ws_live_* ring with waiter shielding, served-past / waited+acked / peak>0+acked LRU eviction, single store_seg_locked() truth shared by blocking write and non-blocking try_write (0/-1/-2 busy). - Conn worker never blocks on storage (busy reply, browser retries); socket idle kill suppressed while readers are parked; stall diagnostics when a write parks. - http_server.c: guarded /api/upload/* REST branch; multipart.c live: scheme adapter (frozen pull path untouched); installer_start_live. Frontend: DirectInstallView + useDirectUpload pusher (FIFO seek queue for parallel installer workers, busy requeue+retry, demand-paging standby until install finalizes, silent-socket watchdog), wired into the history router (#/direct-install, Circle-back closes it). Parser: pkg_parser_parse_mem reuses in-file sub-parsers for live header parsing (differential-tested). Tests: ring unit (test_ws_stream), far-seek/pivot/busy/waiter unit (test_ws_stream_far), transport (test_ws_upload, incl. socket busy->retry), concurrent e2e incl. 90MB/64-slot parallel-pivot replay of the production capture (test_direct_install_e2e), PS5 pull-replay simulator (ps5_sim) + CLI tools. Proven on console: 1GB PKG installs at status='playable' with zero idle kills. Full host suite (13 binaries + CSS) green. * perf(smb): pipeline reads for faster package streaming Use a private SMB session per stream connection and keep up to eight credit-aware reads in flight, with requests up to 512 KiB. Reduce each request to the credits available in the current SMB2 window, including queued requests, so libsmb2 cannot silently shorten a request and leave a gap in the stream. Increase stream and socket buffers, and leave signing optional while still honoring servers that require it. Add SMB timing and throughput logs plus negotiated session diagnostics to separate SMB read latency from HTTP streaming overhead. Include the SMB logger in PS5 and host builds and update the mock async API used by tests. * fix(ws): harden direct install sessions and package preview Tie uploads to a browser owner and one authenticated WebSocket. Resume from acknowledged segments so cache eviction does not lose progress. Move Direct Install into media sources, show local PKG title and icon before installation, and warn before leaving an active transfer. Pass package metadata to the live installer and cover ownership and metadata parsing with tests. * fix(smb): honor package install debug setting Gate SMB debug file creation and per-read diagnostics behind pkg_install_debug, matching WebSocket stream logging behavior. Keep regular SMB package streaming unchanged. * fix(ui): return direct install to drives route Make the Direct Install Back to drives action replace the current route and restore the media source view directly. This prevents browsers that opened the page from leaving the app for an unrelated previous history entry. * fix(ui): restore deep-linked pages on reload Apply the initial hash route to the visible view during startup, including storage media and package details. Load package data for the linked drive so a direct package URL opens its detail page after a reload. * fix(ui): accept package drops across the app Handle package drops at the application level, open Direct Install, and pass the file through the existing package preview flow. Keep active uploads and installations protected from replacement. * fix(direct): check package eligibility before install Share the scanner's installed state and version rules with Direct Install. Check selected package metadata before enabling Install and again before starting a new transfer, while preserving resumed sessions. Show storage and active install blockers on the direct install page. * fix(direct): share live install icon across browsers Upload the parsed package icon once to the active direct install session and serve it from RAM through the existing icon API. Other browsers can resolve the live session path shown in installer status. Clear icon data when the session is canceled or replaced. * fix(direct): cancel install when uploader closes Use sendBeacon during browser teardown to call the authenticated Direct Install cancel endpoint for the sending session. Keep observer browsers unaffected and avoid duplicate cancellation requests. * fix(direct): cancel when uploader socket closes Treat an unexpected close of the authenticated uploader WebSocket before the final finish message as an install cancellation. Preserve normal completed sessions and reduce repetitive seek, eviction, and read-short logging in the main log. * bump version to 1.1.1 * fix(ui): stop duplicate direct upload polling Keep the observer watchdog stable across renders, disable it for the owning upload tab, and prevent overlapping status requests in both polling paths. * Add PS4 and PS5 package badges * feat: support platform-aware install storage * fix: preserve catalog during provisional scans * bump version to 1.2.0 * docs: add changelog for 1.2.0 * chore: remove stale refactor references * feat: move streaming services to dedicated ports * docs: update 1.2.0 documentation
415 lines
16 KiB
C
415 lines
16 KiB
C
/*
|
|
* PS5 Installer Stream Simulator — raw HTTP client half.
|
|
*
|
|
* Self-contained (no dependency on stream_server.c) so the same code can be
|
|
* linked into the host test, built into a standalone CLI, or adapted to a
|
|
* websocket client. It speaks just enough HTTP/1.1 to talk to the stream
|
|
* server: send a GET with a Range header, read status + headers, then read
|
|
* exactly Content-Length body bytes.
|
|
*/
|
|
|
|
#include "ps5_sim.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <time.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <netdb.h>
|
|
#include <arpa/inet.h>
|
|
|
|
/* ---- low-level HTTP plumbing ------------------------------------------- */
|
|
|
|
/* Forward declaration (defined after sim_start_ms below). */
|
|
static uint64_t sim_now_ms(void);
|
|
|
|
static int sim_socket(const char *host, int port) {
|
|
struct addrinfo hints, *res = NULL, *rp;
|
|
char portstr[16];
|
|
int fd = -1;
|
|
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = AF_UNSPEC;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
snprintf(portstr, sizeof(portstr), "%d", port);
|
|
|
|
if (getaddrinfo(host, portstr, &hints, &res) != 0) {
|
|
return -1;
|
|
}
|
|
for (rp = res; rp; rp = rp->ai_next) {
|
|
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
|
if (fd < 0) continue;
|
|
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break; /* success */
|
|
close(fd);
|
|
fd = -1;
|
|
}
|
|
freeaddrinfo(res);
|
|
return fd;
|
|
}
|
|
|
|
/* Read exactly n bytes, returning 0 on success, -1 on short/failed read. */
|
|
static int read_exact(int fd, void *buf, size_t n) {
|
|
char *p = (char *)buf;
|
|
size_t got = 0;
|
|
while (got < n) {
|
|
ssize_t r = recv(fd, p + got, n - got, 0);
|
|
if (r < 0) {
|
|
if (errno == EINTR) continue;
|
|
return -1;
|
|
}
|
|
if (r == 0) return -1; /* peer closed */
|
|
got += (size_t)r;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Structured response returned by sim_http_get(). */
|
|
typedef struct {
|
|
int status; /* 0 on protocol error */
|
|
uint64_t content_len;
|
|
char content_range[128];
|
|
uint8_t *body; /* malloc'd, content_len bytes */
|
|
size_t body_len;
|
|
} sim_resp_t;
|
|
|
|
/*
|
|
* Send one GET request and read the complete response (headers + body).
|
|
* On success returns 0 and fills resp; on protocol failure returns -1.
|
|
*/
|
|
static int sim_http_get(int fd, const char *req, size_t reqlen, sim_resp_t *resp) {
|
|
memset(resp, 0, sizeof(*resp));
|
|
|
|
if (send(fd, req, reqlen, 0) != (ssize_t)reqlen) {
|
|
return -1;
|
|
}
|
|
#ifdef PS5_SIM_DEBUG
|
|
fprintf(stderr, "[SIM-DBG @%llums] sent (%zu): %.200s\n",
|
|
(unsigned long long)sim_now_ms(), reqlen, req);
|
|
#endif
|
|
|
|
/* Read headers until the blank line, one byte at a time. Reading exactly
|
|
* up to (and never past) the terminating CRLFCRLF guarantees a keep-alive
|
|
* response can never have its body bytes absorbed into the header buffer.
|
|
* If it did, those body bytes would be discarded yet read_exact would then
|
|
* wait (for the full SO_RCVTIMEO) for body bytes the server already sent --
|
|
* the keep-alive desync that otherwise makes a correct connection hang. */
|
|
char hdr[8192];
|
|
size_t hdr_len = 0;
|
|
for (;;) {
|
|
char c;
|
|
if (read_exact(fd, &c, 1) != 0) return -1;
|
|
if (hdr_len + 1 >= sizeof(hdr)) return -1; /* header too large */
|
|
hdr[hdr_len++] = c;
|
|
if (hdr_len >= 4 &&
|
|
hdr[hdr_len - 4] == '\r' && hdr[hdr_len - 3] == '\n' &&
|
|
hdr[hdr_len - 2] == '\r' && hdr[hdr_len - 1] == '\n') {
|
|
break;
|
|
}
|
|
}
|
|
/* Null-terminate so the strstr/strncasecmp parser below can't run off the
|
|
* end of the response into stale heap bytes. */
|
|
hdr[hdr_len] = '\0';
|
|
|
|
/* Status line: HTTP/1.1 206 Partial Content */
|
|
char *sp1 = strchr(hdr, ' ');
|
|
if (!sp1) return -1;
|
|
resp->status = atoi(sp1 + 1);
|
|
#ifdef PS5_SIM_DEBUG
|
|
fprintf(stderr, "[SIM-DBG @%llums] got status=%llu clen=%llu crange='%s'\n",
|
|
(unsigned long long)sim_now_ms(),
|
|
(unsigned long long)resp->status, (unsigned long long)resp->content_len, resp->content_range);
|
|
#endif
|
|
|
|
/* Parse Content-Length and Content-Range headers. */
|
|
for (char *scan = hdr; (scan = strstr(scan, "\r\n")) != NULL; ) {
|
|
scan += 2;
|
|
if (strncasecmp(scan, "content-length:", 15) == 0) {
|
|
resp->content_len = strtoull(scan + 15, NULL, 10);
|
|
} else if (strncasecmp(scan, "content-range:", 14) == 0) {
|
|
/* Store the full Content-Range value ("bytes 0-65536/5242880") so a
|
|
* caller can parse the total after the '/'. */
|
|
const char *p = scan + 14;
|
|
while (*p == ' ') p++;
|
|
size_t avail = sizeof(resp->content_range) - 1;
|
|
size_t n = 0;
|
|
while (p[n] && n < avail && p[n] != '\r' && p[n] != '\n') n++;
|
|
memcpy(resp->content_range, p, n);
|
|
resp->content_range[n] = '\0';
|
|
}
|
|
}
|
|
|
|
/* Read the body. */
|
|
if (resp->content_len > 0) {
|
|
resp->body = (uint8_t *)malloc(resp->content_len);
|
|
if (!resp->body) return -1;
|
|
if (read_exact(fd, resp->body, resp->content_len) != 0) {
|
|
free(resp->body);
|
|
resp->body = NULL;
|
|
return -1;
|
|
}
|
|
resp->body_len = resp->content_len;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ---- replay logging ----------------------------------------------------- */
|
|
|
|
static uint64_t sim_start_ms = 0;
|
|
|
|
static uint64_t sim_now_ms(void) {
|
|
struct timeval tv;
|
|
gettimeofday(&tv, NULL);
|
|
uint64_t now = (uint64_t)tv.tv_sec * 1000ULL + (uint64_t)tv.tv_usec / 1000ULL;
|
|
return sim_start_ms ? (now - sim_start_ms) : 0;
|
|
}
|
|
|
|
static void sim_log(FILE *fp, const char *fmt, ...) {
|
|
if (!fp) return;
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vfprintf(fp, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
/* ---- replay phases ------------------------------------------------------ */
|
|
|
|
/* Format "GET <path>?<query> HTTP/1.1\r\n...\r\n\r\n". */
|
|
static char *sim_build_get(const char *path, const char *query, const char *range,
|
|
char *buf, size_t bufsz) {
|
|
int n = snprintf(buf, bufsz,
|
|
"GET %s%s HTTP/1.1\r\n"
|
|
"Host: 127.0.0.1\r\n"
|
|
"Connection: keep-alive\r\n"
|
|
"Accept-Ranges: bytes\r\n"
|
|
"%s"
|
|
"\r\n",
|
|
path, query ? query : "",
|
|
range ? range : "");
|
|
if (n <= 0 || (size_t)n >= bufsz) return NULL;
|
|
return buf;
|
|
}
|
|
|
|
/* One header-acquisition connection (Phase 1 + optional Phase 2 CRC probe). */
|
|
static int sim_one_connection(int conn_id, const ps5_sim_target_t *t,
|
|
const char *query, uint64_t total, uint64_t *delivered,
|
|
FILE *log_fp) {
|
|
char req[4096];
|
|
char range[64];
|
|
int reqs = 0;
|
|
int conn_fail = 0;
|
|
int fd = sim_socket(t->host, t->port);
|
|
if (fd < 0) {
|
|
return -1;
|
|
}
|
|
#ifdef PS5_SIM_DEBUG
|
|
fprintf(stderr, "[SIM-DBG @%llums] conn #%d open fd=%d\n",
|
|
(unsigned long long)sim_now_ms(), conn_id, fd);
|
|
#endif
|
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO,
|
|
(const void *)&((struct timeval){.tv_sec = 30, .tv_usec = 0}),
|
|
sizeof(struct timeval));
|
|
|
|
/* Phase 1: header acquisition (bytes=0-65535), repeated reqs_per_header. */
|
|
for (int h = 0; h < t->reqs_per_header; h++) {
|
|
snprintf(range, sizeof(range), "Range: bytes=0-%u\r\n", (unsigned)PS5_SIM_HEADER_END);
|
|
sim_build_get(t->path, query, range, req, sizeof(req));
|
|
sim_resp_t r;
|
|
if (sim_http_get(fd, req, strlen(req), &r) != 0 || r.status != 206) {
|
|
conn_fail = 1;
|
|
if (log_fp) sim_log(log_fp, "%llu\tREQUEST\t%d\t%d\t127.0.0.1\tGET\t%s\trange\t206?\t%d\tTotal: %llu\n",
|
|
(unsigned long long)sim_now_ms(), conn_id, reqs + 1, t->path, r.status, (unsigned long long)total);
|
|
} else {
|
|
*delivered += r.content_len;
|
|
if (log_fp) sim_log(log_fp, "%llu\tREQUEST\t%d\t%d\t127.0.0.1\tGET\t%s\tRange: bytes=0-%u\t%d\tContent-Length: %llu\tTotal: %llu\n",
|
|
(unsigned long long)sim_now_ms(), conn_id, reqs + 1, t->path,
|
|
(unsigned)PS5_SIM_HEADER_END, r.status, (unsigned long long)r.content_len, (unsigned long long)total);
|
|
sim_log(log_fp, "%llu\tBODY_DONE\t%d\t%d\t127.0.0.1\tsent=%llu/%llu\treason=complete\n",
|
|
(unsigned long long)sim_now_ms(), conn_id, reqs + 1,
|
|
(unsigned long long)r.content_len, (unsigned long long)r.content_len);
|
|
}
|
|
free(r.body);
|
|
reqs++;
|
|
}
|
|
|
|
/* Phase 2: sidecar CRC probe (Range-less) on the same connection. */
|
|
if (t->check_crc && t->content_id && t->content_id[0]) {
|
|
const char *slash = strrchr(t->path, '/');
|
|
int dir = slash ? (int)(slash - t->path) : (int)strlen(t->path);
|
|
char crc_path[700];
|
|
int n = snprintf(crc_path, sizeof(crc_path), "%.*s/%.512s.crc",
|
|
dir, slash ? t->path : "", t->content_id);
|
|
if (n > 0 && (size_t)n < sizeof(crc_path)) {
|
|
sim_build_get(crc_path, query, NULL, req, sizeof(req));
|
|
sim_resp_t r;
|
|
int got_404 = (sim_http_get(fd, req, strlen(req), &r) == 0) && r.status == 404;
|
|
if (log_fp) sim_log(log_fp, "%llu\tREQUEST\t%d\t%d\t127.0.0.1\tGET\t%s\tno-range\t%d\tContent-Length: %llu\tTotal: 0\n",
|
|
(unsigned long long)sim_now_ms(), conn_id, reqs + 1, crc_path,
|
|
r.status, (unsigned long long)r.content_len);
|
|
free(r.body);
|
|
if (!got_404) conn_fail = 1;
|
|
reqs++;
|
|
}
|
|
}
|
|
|
|
if (log_fp) sim_log(log_fp, "%llu\tCONN_CLOSE\t%d\t-\t127.0.0.1\treqs_served=%d\n",
|
|
(unsigned long long)sim_now_ms(), conn_id, reqs);
|
|
|
|
#ifdef PS5_SIM_DEBUG
|
|
fprintf(stderr, "[SIM-DBG @%llums] conn #%d done: reqs=%d conn_fail=%d -> close(fd=%d)\n",
|
|
(unsigned long long)sim_now_ms(), conn_id, reqs, conn_fail, fd);
|
|
#endif
|
|
close(fd);
|
|
return conn_fail ? -1 : 0;
|
|
}
|
|
|
|
/* Phase 3: parallel bulk transfer of contiguous chunks across `parallel`
|
|
* connections, round-robin assigned so the two sockets ping-pong. */
|
|
static int sim_bulk(const ps5_sim_target_t *t, const char *query, uint64_t total,
|
|
uint64_t *delivered, FILE *log_fp) {
|
|
if (total <= PS5_SIM_HEADER_END) {
|
|
return 0; /* nothing past the header to stream */
|
|
}
|
|
|
|
uint64_t chunk = t->chunk_size ? t->chunk_size : PS5_SIM_DEFAULT_CHUNK;
|
|
int parallel = t->parallel > 0 ? t->parallel : 1;
|
|
if (parallel > 8) parallel = 8;
|
|
|
|
int fds[8] = {0};
|
|
int reqs[8] = {0};
|
|
int nconns = parallel;
|
|
for (int c = 0; c < nconns; c++) {
|
|
fds[c] = sim_socket(t->host, t->port);
|
|
if (fds[c] < 0) {
|
|
for (int k = 0; k < c; k++) close(fds[k]);
|
|
return -1;
|
|
}
|
|
setsockopt(fds[c], SOL_SOCKET, SO_RCVTIMEO,
|
|
(const void *)&((struct timeval){.tv_sec = 30, .tv_usec = 0}),
|
|
sizeof(struct timeval));
|
|
}
|
|
|
|
int fail = 0;
|
|
uint64_t next = PS5_SIM_HEADER_END; /* first byte after the header read */
|
|
while (next < total) {
|
|
uint64_t end = next + chunk - 1;
|
|
if (end >= total) end = total - 1;
|
|
int c = (int)((next - PS5_SIM_HEADER_END) % (uint64_t)parallel);
|
|
|
|
char range[96];
|
|
snprintf(range, sizeof(range), "Range: bytes=%llu-%llu\r\n",
|
|
(unsigned long long)next, (unsigned long long)end);
|
|
char req[4096];
|
|
sim_build_get(t->path, query, range, req, sizeof(req));
|
|
sim_resp_t r;
|
|
int ok = (sim_http_get(fds[c], req, strlen(req), &r) == 0) &&
|
|
r.status == 206 &&
|
|
r.content_len == (uint64_t)(end - next + 1);
|
|
if (!ok) {
|
|
fail = 1;
|
|
if (log_fp) sim_log(log_fp, "%llu\tREQUEST\t%d\t%d\t127.0.0.1\tGET\t%s\trange\t%d\tContent-Length: %llu (expected %llu)\n",
|
|
(unsigned long long)sim_now_ms(), c + 1, reqs[c] + 1, t->path, r.status,
|
|
(unsigned long long)r.content_len, (unsigned long long)(end - next + 1));
|
|
} else {
|
|
*delivered += r.content_len;
|
|
if (log_fp) sim_log(log_fp, "%llu\tREQUEST\t%d\t%d\t127.0.0.1\tGET\t%s\tRange: bytes=%llu-%llu\t%d\tContent-Length: %llu\tTotal: %llu\n",
|
|
(unsigned long long)sim_now_ms(), c + 1, reqs[c] + 1, t->path,
|
|
(unsigned long long)next, (unsigned long long)end, r.status,
|
|
(unsigned long long)r.content_len, (unsigned long long)total);
|
|
sim_log(log_fp, "%llu\tBODY_DONE\t%d\t%d\t127.0.0.1\tsent=%llu/%llu\treason=complete\n",
|
|
(unsigned long long)sim_now_ms(), c + 1, reqs[c] + 1,
|
|
(unsigned long long)r.content_len, (unsigned long long)r.content_len);
|
|
}
|
|
free(r.body);
|
|
reqs[c]++;
|
|
next = end + 1;
|
|
}
|
|
|
|
for (int c = 0; c < nconns; c++) {
|
|
if (log_fp) sim_log(log_fp, "%llu\tCONN_CLOSE\t%d\t-\t127.0.0.1\treqs_served=%d\n",
|
|
(unsigned long long)sim_now_ms(), c + 1, reqs[c]);
|
|
close(fds[c]);
|
|
}
|
|
return fail ? -1 : 0;
|
|
}
|
|
|
|
/* ---- public entry point ------------------------------------------------- */
|
|
|
|
int ps5_sim_replay(const ps5_sim_target_t *t, ps5_sim_stats_t *out, FILE *log_fp) {
|
|
memset(out, 0, sizeof(*out));
|
|
struct timeval tv0;
|
|
gettimeofday(&tv0, NULL);
|
|
sim_start_ms = (uint64_t)tv0.tv_sec * 1000ULL + (uint64_t)tv0.tv_usec / 1000ULL;
|
|
|
|
const char *query = t->query && t->query[0] ? t->query : PS5_SIM_DEFAULT_QUERY;
|
|
if (!t->host || !t->path) {
|
|
if (out) out->failures = 1;
|
|
return -1;
|
|
}
|
|
|
|
/* Discover total size from the server if not supplied (read it off the
|
|
* first header request's Content-Range). */
|
|
uint64_t total = t->total_size;
|
|
if (total == 0) {
|
|
char req[4096];
|
|
char range[64];
|
|
snprintf(range, sizeof(range), "Range: bytes=0-%u\r\n", (unsigned)PS5_SIM_HEADER_END);
|
|
sim_build_get(t->path, query, range, req, sizeof(req));
|
|
|
|
int fd = sim_socket(t->host, t->port);
|
|
if (fd >= 0) {
|
|
sim_resp_t r;
|
|
if (sim_http_get(fd, req, strlen(req), &r) == 0) {
|
|
const char *slash = strchr(r.content_range, '/');
|
|
if (slash) total = strtoull(slash + 1, NULL, 10);
|
|
out->bytes_received = r.content_len;
|
|
out->total_delivered += r.content_len;
|
|
out->requests_made++;
|
|
if (log_fp) sim_log(log_fp, "%llu\tREQUEST\t%d\t%d\t127.0.0.1\tGET\t%s\tRange: bytes=0-%u\t%d\tContent-Length: %llu\tTotal: %llu\n",
|
|
(unsigned long long)sim_now_ms(), 0, 1, t->path,
|
|
(unsigned)PS5_SIM_HEADER_END, r.status,
|
|
(unsigned long long)r.content_len, (unsigned long long)total);
|
|
sim_log(log_fp, "%llu\tBODY_DONE\t%d\t%d\t127.0.0.1\tsent=%llu/%llu\treason=complete\n",
|
|
(unsigned long long)sim_now_ms(), 0, 1,
|
|
(unsigned long long)r.content_len, (unsigned long long)r.content_len);
|
|
}
|
|
free(r.body);
|
|
close(fd);
|
|
if (log_fp) sim_log(log_fp, "%llu\tCONN_CLOSE\t%d\t-\t127.0.0.1\treqs_served=1\n",
|
|
(unsigned long long)sim_now_ms(), 0);
|
|
}
|
|
}
|
|
|
|
if (total == 0) {
|
|
if (out) out->failures = 1;
|
|
return -2; /* could not learn size */
|
|
}
|
|
|
|
int phase_fail = 0;
|
|
|
|
/* Phase 1 (+ optional 2): header acquisition, one connection per repeat. */
|
|
for (int i = 0; i < t->header_repeats; i++) {
|
|
int rc = sim_one_connection(i + 1, t, query, total, &out->total_delivered, log_fp);
|
|
out->conns_opened++;
|
|
if (rc != 0) phase_fail = 1;
|
|
}
|
|
out->header_ok = !phase_fail;
|
|
|
|
/* Phase 3: parallel bulk. */
|
|
int bulk_fail = sim_bulk(t, query, total, &out->total_delivered, log_fp);
|
|
out->bulk_ok = (bulk_fail == 0);
|
|
if (bulk_fail) phase_fail = 1;
|
|
|
|
/* CRC sidecar flag (verified inside sim_one_connection when enabled). */
|
|
out->crc_404 = 1;
|
|
|
|
out->failures = phase_fail;
|
|
return phase_fail ? 1 : 0;
|
|
}
|