mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-21 22:33:58 +08:00
643 lines
20 KiB
C
643 lines
20 KiB
C
// Released under the MIT License. See LICENSE for details.
|
|
|
|
// An ultra-simple client app to forward commands to a pcommand server. This
|
|
// lets us run *lots* of small pcommands very fast. Often the limiting
|
|
// factor in such cases is the startup time of Python which this mostly
|
|
// eliminates. See tools/efrotools/pcommandbatch.py for more info.
|
|
|
|
#include <arpa/inet.h>
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <netinet/in.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/param.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include "cJSON.h"
|
|
|
|
struct Context_ {
|
|
const char* state_dir_path;
|
|
const char* project_dir_path;
|
|
const char* instance_prefix;
|
|
int instance_num;
|
|
int pid;
|
|
int verbose;
|
|
int debug;
|
|
int server_idle_seconds;
|
|
const char* pcommandpath;
|
|
int sockfd;
|
|
};
|
|
|
|
int path_exists_(const char* path);
|
|
int establish_connection_(struct Context_* ctx);
|
|
int calc_paths_(struct Context_* ctx);
|
|
int send_command_(struct Context_* ctx, int argc, char** argv);
|
|
int handle_response_(struct Context_* ctx);
|
|
|
|
// Read all data from a socket and return as a malloc'ed null-terminated
|
|
// string.
|
|
char* read_string_from_socket_(const struct Context_* ctx);
|
|
|
|
// Tear down context (closing socket, etc.) before closing app.
|
|
void tear_down_context_(struct Context_* ctx);
|
|
|
|
// If a valid state file is present at the provided path and not older than
|
|
// server_idle_seconds, return said port as an int. Otherwise return -1;
|
|
int get_running_server_port_(const struct Context_* ctx,
|
|
const char* state_file_path_full);
|
|
|
|
int main(int argc, char** argv) {
|
|
struct Context_ ctx;
|
|
memset(&ctx, 0, sizeof(ctx));
|
|
|
|
ctx.server_idle_seconds = 5;
|
|
ctx.pid = getpid();
|
|
ctx.sockfd = -1;
|
|
|
|
// Verbose mode enables more printing here. Debug mode enables that plus
|
|
// extra stuff. The extra stuff is mostly the server side though.
|
|
{
|
|
const char* debug_env = getenv("BA_PCOMMANDBATCH_DEBUG");
|
|
ctx.debug = debug_env && !strcmp(debug_env, "1");
|
|
const char* verbose_env = getenv("BA_PCOMMANDBATCH_VERBOSE");
|
|
ctx.verbose = ctx.debug || (verbose_env && !strcmp(verbose_env, "1"));
|
|
}
|
|
|
|
// Seed rand() using the current time in microseconds.
|
|
struct timeval tv;
|
|
gettimeofday(&tv, NULL);
|
|
unsigned int seed = tv.tv_usec;
|
|
srand(seed);
|
|
|
|
// Figure our which file path we'll use to get server state.
|
|
if (calc_paths_(&ctx) != 0) {
|
|
tear_down_context_(&ctx);
|
|
return 1;
|
|
}
|
|
|
|
// Establish communication with said server (spinning it up if needed).
|
|
ctx.sockfd = establish_connection_(&ctx);
|
|
if (ctx.sockfd == -1) {
|
|
tear_down_context_(&ctx);
|
|
return 1;
|
|
}
|
|
|
|
if (send_command_(&ctx, argc, argv) != 0) {
|
|
tear_down_context_(&ctx);
|
|
return 1;
|
|
}
|
|
|
|
int result_val = handle_response_(&ctx);
|
|
if (result_val != 0) {
|
|
tear_down_context_(&ctx);
|
|
return 1;
|
|
}
|
|
|
|
tear_down_context_(&ctx);
|
|
return result_val;
|
|
}
|
|
|
|
void tear_down_context_(struct Context_* ctx) {
|
|
if (ctx->sockfd != -1) {
|
|
if (close(ctx->sockfd) != 0) {
|
|
fprintf(stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): error %d closing "
|
|
"socket.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid, errno);
|
|
}
|
|
}
|
|
}
|
|
|
|
int get_running_server_port_(const struct Context_* ctx,
|
|
const char* state_file_path_full) {
|
|
struct stat file_stat;
|
|
|
|
time_t current_time = time(NULL);
|
|
if (current_time == -1) {
|
|
perror("time");
|
|
return -1;
|
|
}
|
|
|
|
int fd = open(state_file_path_full, O_RDONLY);
|
|
if (fd < 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (fstat(fd, &file_stat) == -1) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
int age_seconds = current_time - file_stat.st_mtime;
|
|
if (ctx->verbose) {
|
|
if (age_seconds <= ctx->server_idle_seconds) {
|
|
fprintf(stderr,
|
|
"pcommandbatch client %s_%d (pid %d) found state file with age "
|
|
"%d at "
|
|
"time %ld.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid, age_seconds,
|
|
time(NULL));
|
|
}
|
|
}
|
|
|
|
if (age_seconds > ctx->server_idle_seconds) {
|
|
close(fd);
|
|
return -1;
|
|
} else if (age_seconds < 0) {
|
|
fprintf(stderr, "pcommandbatch got negative age; unexpected.");
|
|
}
|
|
|
|
char buf[256];
|
|
ssize_t amt = read(fd, buf, sizeof(buf) - 1);
|
|
close(fd);
|
|
|
|
if (amt == -1 || amt == sizeof(buf) - 1) {
|
|
return -1;
|
|
}
|
|
buf[amt] = 0; // Null-terminate it.
|
|
|
|
cJSON* state_dict = cJSON_Parse(buf);
|
|
if (!state_dict) {
|
|
// An un-parseable state file is not a recoverable error; go down hard.
|
|
// State files are written and then moved into place so we should never
|
|
// see something like a half-written file.
|
|
fprintf(stderr,
|
|
"Fatal Error: pcommandbatch client %s_%d (pid %d):"
|
|
" failed to parse state file value of size %zu.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid, amt);
|
|
fflush(stderr);
|
|
abort();
|
|
// return -1;
|
|
}
|
|
|
|
// If results included output, print it.
|
|
cJSON* port_obj = cJSON_GetObjectItem(state_dict, "p");
|
|
if (!port_obj || !cJSON_IsNumber(port_obj)) {
|
|
fprintf(stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): failed to get port "
|
|
"value from state.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid);
|
|
cJSON_Delete(state_dict);
|
|
return -1;
|
|
}
|
|
int port = cJSON_GetNumberValue(port_obj);
|
|
cJSON_Delete(state_dict);
|
|
return port;
|
|
}
|
|
|
|
int path_exists_(const char* path) {
|
|
struct stat file_stat;
|
|
return (stat(path, &file_stat) != -1);
|
|
}
|
|
|
|
int establish_connection_(struct Context_* ctx) {
|
|
char state_file_path_full[256];
|
|
snprintf(state_file_path_full, sizeof(state_file_path_full),
|
|
"%s/worker_state_%s_%d", ctx->state_dir_path, ctx->instance_prefix,
|
|
ctx->instance_num);
|
|
|
|
int sockfd = 0;
|
|
|
|
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
|
|
fprintf(stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): could not create "
|
|
"socket.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid);
|
|
return -1;
|
|
}
|
|
|
|
// On Mac I'm running into EADDRNOTAVAIL errors if I spit out too many
|
|
// requests in a short enough period of time. I'm guessing its exhausting
|
|
// free ports when cooldown time is taken into account. Sleeping and
|
|
// trying again in a moment seems to work.
|
|
int retry_attempt = 0;
|
|
int retry_sleep_secs = 1;
|
|
while (1) {
|
|
// First look for an already-running batch server.
|
|
int port = get_running_server_port_(ctx, state_file_path_full);
|
|
if (port == -1) {
|
|
// Ok; no running server. Spin one up.
|
|
if (ctx->verbose) {
|
|
fprintf(stderr,
|
|
"pcommandbatch client %s_%d (pid %d) requesting batch server "
|
|
"spinup...\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid);
|
|
}
|
|
|
|
// In non-debug-mode, route to a log file.
|
|
char endbuf[1024];
|
|
if (ctx->debug) {
|
|
snprintf(endbuf, sizeof(endbuf), " &");
|
|
} else {
|
|
snprintf(endbuf, sizeof(endbuf), " >>%s/worker_log_%s_%d 2>&1 &",
|
|
ctx->state_dir_path, ctx->instance_prefix, ctx->instance_num);
|
|
}
|
|
char buf[2048];
|
|
snprintf(buf, sizeof(buf),
|
|
"%s batchserver --timeout %d --project-dir %s"
|
|
" --instance %s_%d %s",
|
|
ctx->pcommandpath, ctx->server_idle_seconds,
|
|
ctx->project_dir_path, ctx->instance_prefix, ctx->instance_num,
|
|
endbuf);
|
|
system(buf);
|
|
|
|
// Spin and wait up to a few seconds for the file to appear.
|
|
time_t start_time = time(NULL);
|
|
int cycles = 0;
|
|
while (time(NULL) - start_time < 5) {
|
|
port = get_running_server_port_(ctx, state_file_path_full);
|
|
if (port != -1) {
|
|
break;
|
|
}
|
|
usleep(10000);
|
|
cycles += 1;
|
|
}
|
|
if (ctx->verbose) {
|
|
fprintf(stderr,
|
|
"pcommandbatch client %s_%d (pid %d) waited %d"
|
|
" cycles for state file to appear at '%s'.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid, cycles,
|
|
state_file_path_full);
|
|
}
|
|
|
|
if (port == -1) {
|
|
// We failed but we can retry.
|
|
if (ctx->verbose) {
|
|
fprintf(stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): failed to open "
|
|
"server on attempt %d.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid,
|
|
retry_attempt);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ok we got a port; now try to connect to it.
|
|
if (port != -1) {
|
|
if (ctx->verbose) {
|
|
fprintf(stderr,
|
|
"pcommandbatch client %s_%d (pid %d) will use server on port "
|
|
"%d at "
|
|
"time %ld.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid, port,
|
|
time(NULL));
|
|
}
|
|
|
|
struct sockaddr_in serv_addr;
|
|
memset(&serv_addr, '0', sizeof(serv_addr));
|
|
serv_addr.sin_family = AF_INET;
|
|
serv_addr.sin_port = htons(port);
|
|
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
|
|
|
int cresult =
|
|
connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
|
|
if (cresult == 0) {
|
|
break;
|
|
} else if (errno == EADDRNOTAVAIL) {
|
|
// Seems we can get this if blasting the machine with enough
|
|
// commands that they run out of ports for us to use. The situation
|
|
// should resolve itself if we wait/retry a few times.
|
|
if (ctx->verbose) {
|
|
fprintf(stderr,
|
|
"pcommandbatch client %s_%d (pid %d): got EADDRNOTAVAIL"
|
|
" on connect attempt %d.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid,
|
|
retry_attempt + 1);
|
|
}
|
|
} else if (errno == ECONNREFUSED) {
|
|
// Am seeing these very rarely on random one-off commands. I'm
|
|
// guessing there's some race condition at the OS level where the
|
|
// port-file write goes through before the socket is actually truly
|
|
// accepting connections. A retry should succeed.
|
|
if (ctx->verbose) {
|
|
fprintf(stderr,
|
|
"pcommandbatch client %s_%d (pid %d): got ECONNREFUSED"
|
|
" on connect attempt %d.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid,
|
|
retry_attempt + 1);
|
|
}
|
|
} else if (errno == EINVAL) {
|
|
// Saw this randomly once on Mac. Not sure what could have led to it.
|
|
if (ctx->verbose) {
|
|
fprintf(stderr,
|
|
"pcommandbatch client %s_%d (pid %d): got EINVAL"
|
|
" on connect attempt %d.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid,
|
|
retry_attempt + 1);
|
|
}
|
|
} else {
|
|
// Currently not retrying on other errors.
|
|
fprintf(stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): connect failed "
|
|
"(errno "
|
|
"%d).\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid, errno);
|
|
close(sockfd);
|
|
return -1;
|
|
}
|
|
}
|
|
// Let's stop at 5, which will be about a minute of waiting total.
|
|
if (retry_attempt >= 5) {
|
|
fprintf(stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): too many "
|
|
"retry attempts; giving up.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid);
|
|
close(sockfd);
|
|
return -1;
|
|
}
|
|
|
|
// Am currently seeing the occasional hang in this loop. Let's flip
|
|
// into verbose if that might be happening to diagnose.
|
|
ctx->verbose = 1;
|
|
|
|
if (ctx->verbose) {
|
|
fprintf(
|
|
stderr,
|
|
"pcommandbatch client %s_%d (pid %d) connection attempt %d failed;"
|
|
" will sleep %d secs and try again.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid, retry_attempt + 1,
|
|
retry_sleep_secs);
|
|
}
|
|
sleep(retry_sleep_secs);
|
|
retry_attempt += 1;
|
|
retry_sleep_secs *= 2;
|
|
}
|
|
return sockfd;
|
|
}
|
|
|
|
int calc_paths_(struct Context_* ctx) {
|
|
// Because the server needs to be in the same cwd as we are for things to
|
|
// work, we only support a specific few locations to run from. Currently
|
|
// this is project-root and src/assets
|
|
if (path_exists_("config/projectconfig.json")) {
|
|
// Looks like we're in project root.
|
|
ctx->project_dir_path = ".";
|
|
ctx->state_dir_path = ".cache/pcommandbatch";
|
|
ctx->instance_prefix = "root";
|
|
ctx->pcommandpath = "tools/pcommand";
|
|
} else if (path_exists_("ba_data")
|
|
&& path_exists_("../../config/projectconfig.json")) {
|
|
// Looks like we're in src/assets.
|
|
ctx->project_dir_path = "../..";
|
|
ctx->state_dir_path = "../../.cache/pcommandbatch";
|
|
ctx->instance_prefix = "assets";
|
|
ctx->pcommandpath = "../../tools/pcommand";
|
|
}
|
|
if (ctx->state_dir_path == NULL) {
|
|
char cwdbuf[MAXPATHLEN];
|
|
if (getcwd(cwdbuf, sizeof(cwdbuf)) < 0) {
|
|
fprintf(stderr,
|
|
"Error: pcommandbatch client %s (pid %d): unable to get cwd.\n",
|
|
ctx->instance_prefix, ctx->pid);
|
|
return -1;
|
|
}
|
|
fprintf(stderr,
|
|
"Error: pcommandbatch client %s (pid %d): pcommandbatch from cwd "
|
|
"'%s' "
|
|
"is not supported.\n",
|
|
ctx->instance_prefix, ctx->pid, cwdbuf);
|
|
return -1;
|
|
}
|
|
assert(ctx->pcommandpath != NULL);
|
|
assert(ctx->instance_prefix != NULL);
|
|
|
|
// Spread requests for each location out randomly across a few instances.
|
|
// This greatly increases scalability though is probably wasteful when
|
|
// running just a few commands since we likely spin up a new server for
|
|
// each. Maybe there's some way to smartly scale this. The best setup
|
|
// might be to have a single 'controller' server instance that spins up
|
|
// worker instances as needed. Though such a fancy setup might be
|
|
// overkill.
|
|
// ctx->instance_num = rand() % 6;
|
|
|
|
// I was wondering if using pid would lead to a more even distribution,
|
|
// but it didn't make a significant difference in my tests. And I worry
|
|
// there would be some odd corner case where pid isn't going up evenly, so
|
|
// sticking with rand() for now. ctx->instance_num = ctx->pid % 6;
|
|
|
|
// Actually I think this might be a good technique. This should deliver a
|
|
// few consecutive requests to a single server instance so it should
|
|
// reduce wasted spinup time when just a command or two is run, but it
|
|
// should still scale up to use all 6 instances when lots of commands go
|
|
// through. (tests show this to be the same speed as the others in that
|
|
// latter case).
|
|
ctx->instance_num = (ctx->pid / 4) % 6;
|
|
return 0;
|
|
}
|
|
|
|
int color_enabled() {
|
|
// This logic here should line up with how the 'color_enabled' val in
|
|
// efro.terminal is calculated.
|
|
|
|
// Allow explict enabling/disabling via this env var.
|
|
const char* env = getenv("EFRO_TERMCOLORS");
|
|
if (env && !strcmp(env, "1")) {
|
|
return 1;
|
|
}
|
|
if (env && !strcmp(env, "0")) {
|
|
return 0;
|
|
}
|
|
|
|
env = getenv("TERM");
|
|
|
|
// If TERM is unset, don't attempt color (this is currently the case
|
|
// in xcode).
|
|
if (!env) {
|
|
return 0;
|
|
}
|
|
|
|
// A common way to say the terminal can't do fancy stuff like color.
|
|
if (env && !(strcmp(env, "dumb"))) {
|
|
return 0;
|
|
}
|
|
|
|
// If our stdout is not attached to a terminal, go with no-color.
|
|
if (!isatty(1)) {
|
|
return 0;
|
|
}
|
|
|
|
// We seem to be a terminal with color support; let's do it!
|
|
return 1;
|
|
}
|
|
|
|
int send_command_(struct Context_* ctx, int argc, char** argv) {
|
|
// Build a json array of our args.
|
|
cJSON* req = cJSON_CreateObject();
|
|
cJSON* array = cJSON_CreateArray();
|
|
for (int i = 0; i < argc; ++i) {
|
|
cJSON_AddItemToArray(array, cJSON_CreateString(argv[i]));
|
|
}
|
|
cJSON_AddItemToObject(req, "a", array);
|
|
cJSON_AddItemToObject(
|
|
req, "c", color_enabled() ? cJSON_CreateTrue() : cJSON_CreateFalse());
|
|
char* json_out = cJSON_Print(req);
|
|
|
|
// Send our command.
|
|
int msglen = strlen(json_out);
|
|
if (write(ctx->sockfd, json_out, msglen) != msglen) {
|
|
fprintf(stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): write failed.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid);
|
|
return -1;
|
|
}
|
|
|
|
// Issue a write shutdown so they get EOF on the other end.
|
|
if (shutdown(ctx->sockfd, SHUT_WR) < 0) {
|
|
fprintf(stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): write shutdown "
|
|
"failed.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid);
|
|
return -1;
|
|
}
|
|
|
|
// Clean up our mess after we've sent them on their way.
|
|
free(json_out);
|
|
cJSON_Delete(req);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int handle_response_(struct Context_* ctx) {
|
|
char* inbuf = read_string_from_socket_(ctx);
|
|
|
|
// Getting null or an empty string response imply something is broken.
|
|
if (!inbuf || inbuf[0] == 0) {
|
|
fprintf(stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): failed to read result "
|
|
"(errno %d).\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid, errno);
|
|
if (inbuf) {
|
|
free(inbuf);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
cJSON* result_dict = cJSON_Parse(inbuf);
|
|
|
|
if (!result_dict) {
|
|
fprintf(
|
|
stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): failed to parse result "
|
|
"value: %s\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid, inbuf);
|
|
free(inbuf);
|
|
return -1;
|
|
} else {
|
|
free(inbuf);
|
|
}
|
|
|
|
// If results included stdout output, print it.
|
|
cJSON* result_output = cJSON_GetObjectItem(result_dict, "o");
|
|
if (!result_output || !cJSON_IsString(result_output)) {
|
|
fprintf(
|
|
stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): failed to parse result "
|
|
"output value.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid);
|
|
cJSON_Delete(result_dict);
|
|
return -1;
|
|
}
|
|
char* output_str = cJSON_GetStringValue(result_output);
|
|
assert(output_str);
|
|
if (output_str[0] != 0) {
|
|
printf("%s", output_str);
|
|
}
|
|
|
|
// If results included stderr output, print it.
|
|
result_output = cJSON_GetObjectItem(result_dict, "e");
|
|
if (!result_output || !cJSON_IsString(result_output)) {
|
|
fprintf(
|
|
stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): failed to parse result "
|
|
"output value.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid);
|
|
cJSON_Delete(result_dict);
|
|
return -1;
|
|
}
|
|
output_str = cJSON_GetStringValue(result_output);
|
|
assert(output_str);
|
|
if (output_str[0] != 0) {
|
|
fprintf(stderr, "%s", output_str);
|
|
}
|
|
|
|
cJSON* result_code = cJSON_GetObjectItem(result_dict, "r");
|
|
if (!result_code || !cJSON_IsNumber(result_code)) {
|
|
fprintf(
|
|
stderr,
|
|
"Error: pcommandbatch client %s_%d (pid %d): failed to parse result "
|
|
"code value.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid);
|
|
cJSON_Delete(result_dict);
|
|
return -1;
|
|
}
|
|
int result_val = cJSON_GetNumberValue(result_code);
|
|
if (ctx->verbose) {
|
|
fprintf(stderr, "pcommandbatch client %s_%d (pid %d) final result is %d.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid, result_val);
|
|
}
|
|
cJSON_Delete(result_dict);
|
|
|
|
return result_val;
|
|
}
|
|
|
|
char* read_string_from_socket_(const struct Context_* ctx) {
|
|
const size_t initial_buffer_size = 1024 * 10;
|
|
char* buffer = NULL;
|
|
size_t buffer_size = 0;
|
|
size_t data_received = 0;
|
|
|
|
// Allocate initial memory for the buffer
|
|
buffer = malloc(initial_buffer_size);
|
|
if (!buffer) {
|
|
perror("Failed to allocate memory for buffer");
|
|
return NULL;
|
|
}
|
|
buffer_size = initial_buffer_size;
|
|
|
|
while (1) {
|
|
// Read data from the socket.
|
|
ssize_t bytes_read = recv(ctx->sockfd, buffer + data_received,
|
|
buffer_size - data_received - 1, 0);
|
|
if (bytes_read == -1) {
|
|
perror("Error reading socket data");
|
|
free(buffer);
|
|
return NULL;
|
|
} else if (bytes_read == 0) {
|
|
// Connection closed.
|
|
break;
|
|
}
|
|
|
|
data_received += bytes_read;
|
|
|
|
// Check if buffer is full (reserving space for term char); resize if
|
|
// necessary.
|
|
if (data_received + 1 >= buffer_size) {
|
|
buffer_size *= 2;
|
|
char* rbuffer = (char*)realloc(buffer, buffer_size);
|
|
if (rbuffer) {
|
|
buffer = rbuffer;
|
|
} else {
|
|
perror("Failed to resize buffer");
|
|
free(buffer);
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
assert(data_received + 1 < buffer_size);
|
|
if (ctx->verbose) {
|
|
fprintf(stderr,
|
|
"pcommandbatch client %s_%d (pid %d) read %zu byte response.\n",
|
|
ctx->instance_prefix, ctx->instance_num, ctx->pid, data_received);
|
|
}
|
|
|
|
buffer[data_received] = 0; // Null terminator.
|
|
return buffer;
|
|
}
|