Skip to content

Commit f216a10

Browse files
authored
Merge pull request #2589 from DeusData/fix/cli-flake-diagnostics
fix(cli): failed updates and the scope fixture name their cause
2 parents 83edbd3 + 7547801 commit f216a10

4 files changed

Lines changed: 409 additions & 72 deletions

File tree

‎src/cli/activation_transaction.c‎

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ void cbm_activation_transaction_note_refusal_for_testing(const char *predicate,
138138
activation_note_refusal(predicate, os_error);
139139
}
140140
}
141+
142+
/* Test seam: the next `count` staging writes fail with errno = os_error, so a
143+
* caller's report of a full disk is testable without filling one. */
144+
static unsigned int activation_write_failures_for_testing;
145+
static int activation_write_failure_errno_for_testing;
146+
147+
void cbm_activation_transaction_write_failures_set_for_testing(unsigned int count, int os_error) {
148+
activation_write_failures_for_testing = count;
149+
activation_write_failure_errno_for_testing = os_error;
150+
}
141151
#endif
142152

143153
#ifdef _WIN32
@@ -1134,6 +1144,13 @@ static bool activation_native_sync(activation_native_file_t file) {
11341144

11351145
static bool activation_native_write_all(activation_native_file_t file, const void *data,
11361146
size_t length) {
1147+
#ifdef CBM_ENABLE_TEST_SEAMS
1148+
if (activation_write_failures_for_testing > 0) {
1149+
activation_write_failures_for_testing--;
1150+
errno = activation_write_failure_errno_for_testing;
1151+
return false;
1152+
}
1153+
#endif
11371154
const unsigned char *cursor = data;
11381155
while (length > 0) {
11391156
#ifdef _WIN32
@@ -1618,6 +1635,16 @@ static void activation_failed_stage_cleanup(cbm_activation_transaction_t *transa
16181635
}
16191636
}
16201637

1638+
/* A failed stage write reports IO with errno restored, after the cleanup, to
1639+
* the error of the call that failed (0 when none applies), so the caller can
1640+
* name it. */
1641+
static cbm_activation_transaction_status_t activation_stage_io_failure(
1642+
cbm_activation_transaction_t *transaction, int os_error) {
1643+
activation_failed_stage_cleanup(transaction);
1644+
errno = os_error;
1645+
return CBM_ACTIVATION_TRANSACTION_IO;
1646+
}
1647+
16211648
cbm_activation_transaction_status_t cbm_activation_transaction_stage_bytes(
16221649
const char *target_path, const void *candidate, size_t candidate_size,
16231650
cbm_activation_transaction_t **transaction_out) {
@@ -1643,11 +1670,20 @@ cbm_activation_transaction_status_t cbm_activation_transaction_stage_bytes(
16431670
}
16441671
transaction->staged_exists = true;
16451672
bool written = activation_native_write_all(staged, candidate, candidate_size);
1673+
int os_error = written ? 0 : errno;
16461674
bool durable = written && activation_native_sync(staged);
1675+
if (written && !durable) {
1676+
os_error = errno;
1677+
}
16471678
bool closed = activation_native_close(staged);
1648-
if (!written || !durable || !closed || !activation_sync_directory(transaction)) {
1649-
activation_failed_stage_cleanup(transaction);
1650-
return CBM_ACTIVATION_TRANSACTION_IO;
1679+
if (!closed && os_error == 0) {
1680+
os_error = errno;
1681+
}
1682+
if (!written || !durable || !closed) {
1683+
return activation_stage_io_failure(transaction, os_error);
1684+
}
1685+
if (!activation_sync_directory(transaction)) {
1686+
return activation_stage_io_failure(transaction, errno);
16511687
}
16521688
*transaction_out = transaction;
16531689
return CBM_ACTIVATION_TRANSACTION_OK;
@@ -1797,28 +1833,45 @@ cbm_activation_transaction_status_t cbm_activation_transaction_stage_file(
17971833
unsigned char buffer[64U * 1024U];
17981834
size_t total = 0;
17991835
bool copied = true;
1836+
int os_error = 0;
18001837
for (;;) {
18011838
size_t amount = 0;
18021839
if (!activation_native_read(source, buffer, sizeof(buffer), &amount)) {
18031840
copied = false;
1841+
os_error = errno;
18041842
break;
18051843
}
18061844
if (amount == 0) {
18071845
break;
18081846
}
1809-
if (SIZE_MAX - total < amount || !activation_native_write_all(staged, buffer, amount)) {
1847+
if (SIZE_MAX - total < amount) {
18101848
copied = false;
18111849
break;
18121850
}
1851+
if (!activation_native_write_all(staged, buffer, amount)) {
1852+
copied = false;
1853+
os_error = errno;
1854+
break;
1855+
}
18131856
total += amount;
18141857
}
18151858
bool durable = copied && total > 0 && activation_native_sync(staged);
1859+
if (copied && total > 0 && !durable) {
1860+
os_error = errno;
1861+
}
18161862
bool source_closed = activation_native_close(source);
1863+
if (!source_closed && os_error == 0) {
1864+
os_error = errno;
1865+
}
18171866
bool staged_closed = activation_native_close(staged);
1818-
if (!copied || total == 0 || !durable || !source_closed || !staged_closed ||
1819-
!activation_sync_directory(transaction)) {
1820-
activation_failed_stage_cleanup(transaction);
1821-
return CBM_ACTIVATION_TRANSACTION_IO;
1867+
if (!staged_closed && os_error == 0) {
1868+
os_error = errno;
1869+
}
1870+
if (!copied || total == 0 || !durable || !source_closed || !staged_closed) {
1871+
return activation_stage_io_failure(transaction, os_error);
1872+
}
1873+
if (!activation_sync_directory(transaction)) {
1874+
return activation_stage_io_failure(transaction, errno);
18221875
}
18231876
*transaction_out = transaction;
18241877
return CBM_ACTIVATION_TRANSACTION_OK;

‎src/cli/activation_transaction.h‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ void cbm_activation_transaction_set_before_absent_publish_for_test(
4545
void cbm_activation_transaction_rename_failures_set_for_test(unsigned int count);
4646

4747
/* Stage a candidate beside target_path (therefore on the same filesystem).
48-
* The staged file is private to the current account and executable. */
48+
* The staged file is private to the current account and executable.
49+
* On POSIX, an IO result leaves errno at the error of the failing system call
50+
* (0 when none applies, e.g. a short read), through the stage cleanup, so a
51+
* caller can name it: a full disk reads "No space left on device". The same
52+
* holds for stage_file below. */
4953
cbm_activation_transaction_status_t cbm_activation_transaction_stage_bytes(
5054
const char *target_path, const void *candidate, size_t candidate_size,
5155
cbm_activation_transaction_t **transaction_out);
@@ -100,6 +104,7 @@ const char *cbm_activation_transaction_refusal_note(void);
100104
#ifdef CBM_ENABLE_TEST_SEAMS
101105
void cbm_activation_transaction_note_refusal_for_testing(const char *predicate,
102106
unsigned long os_error);
107+
void cbm_activation_transaction_write_failures_set_for_testing(unsigned int count, int os_error);
103108
#endif
104109

105110
#endif /* CBM_ACTIVATION_TRANSACTION_H */

‎src/cli/cli.c‎

Lines changed: 114 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13202,6 +13202,45 @@ static int cli_update_activate_binary(void *opaque) {
1320213202
return CLI_OK;
1320313203
}
1320413204

13205+
#ifdef __APPLE__
13206+
/* The first step of the signed-candidate preparation that failed. One boolean
13207+
* used to cover all of them, so a full disk read exactly like a broken
13208+
* codesign: "candidate preparation failed", and nothing else. */
13209+
typedef struct {
13210+
const char *step;
13211+
int os_error; /* errno left by an OS-level step; 0 when none applies */
13212+
const char *detail; /* the reason when no OS error applies, or NULL */
13213+
} cli_update_prep_failure_t;
13214+
13215+
/* Records the first failing step. An OS-level step keeps the errno it left; a
13216+
* step that succeeds clears errno so the next one starts clean. Returns ok, so
13217+
* the steps chain with &&. */
13218+
static bool cli_update_prep_step(bool ok, const char *step, bool os_level, const char *detail,
13219+
cli_update_prep_failure_t *failure) {
13220+
if (ok) {
13221+
errno = 0;
13222+
return true;
13223+
}
13224+
if (!failure->step) {
13225+
failure->step = step;
13226+
failure->os_error = os_level ? errno : 0;
13227+
failure->detail = detail;
13228+
}
13229+
return false;
13230+
}
13231+
13232+
static void cli_update_prep_report(const cli_update_prep_failure_t *failure) {
13233+
const char *step = failure->step ? failure->step : "an unrecorded step";
13234+
const char *reason = failure->os_error != 0 ? strerror(failure->os_error) : failure->detail;
13235+
if (reason) {
13236+
(void)fprintf(stderr, "error: signed update candidate preparation failed while %s: %s\n",
13237+
step, reason);
13238+
} else {
13239+
(void)fprintf(stderr, "error: signed update candidate preparation failed while %s\n", step);
13240+
}
13241+
}
13242+
#endif
13243+
1320513244
static int extract_and_install_binary(extract_install_args_t args) {
1320613245
const char *tmp_archive = args.tmp_archive;
1320713246
const char *ext = args.ext;
@@ -13259,42 +13298,79 @@ static int extract_and_install_binary(extract_install_args_t args) {
1325913298
#ifdef __APPLE__
1326013299
/* codesign replaces the signed file on current macOS releases. Publish and
1326113300
* sign a disposable private copy first, then stage the resulting immutable
13262-
* bytes into the final transaction. */
13301+
* bytes into the final transaction. Every step records why it failed, so a
13302+
* full disk is reported as one and not as a signing failure. */
13303+
cli_update_prep_failure_t failure = {0};
1326313304
char prepared_dir[CLI_BUF_1K];
1326413305
char prepared_candidate[CLI_BUF_1K];
1326513306
int prepared_dir_length =
1326613307
snprintf(prepared_dir, sizeof(prepared_dir), "%s/cbm-update-sign-XXXXXX", cbm_tmpdir());
13267-
bool prepared = prepared_dir_length > 0 && (size_t)prepared_dir_length < sizeof(prepared_dir) &&
13268-
cbm_mkdtemp(prepared_dir) != NULL;
13308+
errno = 0;
13309+
bool prepared = cli_update_prep_step(prepared_dir_length > 0 &&
13310+
(size_t)prepared_dir_length < sizeof(prepared_dir) &&
13311+
cbm_mkdtemp(prepared_dir) != NULL,
13312+
"creating its private directory", true, NULL, &failure);
1326913313
int prepared_candidate_length = prepared
1327013314
? snprintf(prepared_candidate, sizeof(prepared_candidate),
1327113315
"%s/codebase-memory-mcp", prepared_dir)
1327213316
: CLI_ERR;
13273-
prepared = prepared && prepared_candidate_length > 0 &&
13274-
(size_t)prepared_candidate_length < sizeof(prepared_candidate);
13317+
prepared = prepared && cli_update_prep_step(prepared_candidate_length > 0 &&
13318+
(size_t)prepared_candidate_length <
13319+
sizeof(prepared_candidate),
13320+
"naming the candidate file", false, NULL, &failure);
1327513321
cbm_activation_transaction_t *preparation = NULL;
1327613322
stage_status = prepared ? cbm_activation_transaction_stage_bytes(prepared_candidate, bin_data,
1327713323
(size_t)bin_len, &preparation)
1327813324
: CBM_ACTIVATION_TRANSACTION_IO;
13325+
prepared = prepared &&
13326+
cli_update_prep_step(
13327+
stage_status == CBM_ACTIVATION_TRANSACTION_OK && preparation,
13328+
"writing the unsigned candidate", stage_status == CBM_ACTIVATION_TRANSACTION_IO,
13329+
cbm_activation_transaction_status_message(stage_status), &failure);
1327913330
free(bin_data);
1328013331
cli_binary_validator_t unsigned_validator = {{0}};
13281-
prepared = stage_status == CBM_ACTIVATION_TRANSACTION_OK && preparation &&
13282-
cli_activation_transaction_expected_build(preparation, &unsigned_validator) &&
13283-
cli_activation_transaction_commit_validated(preparation, &unsigned_validator,
13284-
CLI_OCTAL_PERM) == CLI_OK &&
13285-
cli_activation_transaction_finalize_close(&preparation) == CLI_OK &&
13286-
cbm_macos_adhoc_sign(prepared_candidate) == CLI_OK &&
13287-
cbm_daemon_build_fingerprint_file(prepared_candidate, validator.fingerprint);
13332+
prepared = prepared &&
13333+
cli_update_prep_step(
13334+
cli_activation_transaction_expected_build(preparation, &unsigned_validator),
13335+
"fingerprinting the unsigned candidate", true, NULL, &failure) &&
13336+
cli_update_prep_step(cli_activation_transaction_commit_validated(
13337+
preparation, &unsigned_validator, CLI_OCTAL_PERM) == CLI_OK,
13338+
"publishing the unsigned candidate", false, NULL, &failure) &&
13339+
cli_update_prep_step(
13340+
cli_activation_transaction_finalize_close(&preparation) == CLI_OK,
13341+
"closing the unsigned candidate's transaction", false, NULL, &failure) &&
13342+
cli_update_prep_step(cbm_macos_adhoc_sign(prepared_candidate) == CLI_OK,
13343+
"signing the candidate ad hoc", false, NULL, &failure) &&
13344+
cli_update_prep_step(
13345+
cbm_daemon_build_fingerprint_file(prepared_candidate, validator.fingerprint),
13346+
"fingerprinting the signed candidate", true, NULL, &failure);
1328813347
const char *prepared_argv[] = {prepared_candidate, "--version", NULL};
13289-
prepared = prepared && cbm_exec_no_shell(prepared_argv) == CLI_OK;
13348+
int version_rc = prepared ? cbm_exec_no_shell(prepared_argv) : CLI_OK;
13349+
char version_detail[80];
13350+
if (version_rc == CBM_NOT_FOUND) {
13351+
snprintf(version_detail, sizeof(version_detail), "it could not run or was killed");
13352+
} else {
13353+
snprintf(version_detail, sizeof(version_detail), "--version exited with status %d",
13354+
version_rc);
13355+
}
13356+
prepared =
13357+
prepared && cli_update_prep_step(version_rc == CLI_OK, "running the signed candidate",
13358+
false, version_detail, &failure);
1329013359
if (prepared) {
1329113360
stage_status = cbm_activation_transaction_stage_file(bin_dest, prepared_candidate,
1329213361
&binary_transaction);
1329313362
cli_binary_validator_t staged_validator = {{0}};
1329413363
prepared =
13295-
stage_status == CBM_ACTIVATION_TRANSACTION_OK && binary_transaction &&
13296-
cli_activation_transaction_expected_build(binary_transaction, &staged_validator) &&
13297-
strcmp(staged_validator.fingerprint, validator.fingerprint) == 0;
13364+
cli_update_prep_step(
13365+
stage_status == CBM_ACTIVATION_TRANSACTION_OK && binary_transaction,
13366+
"staging the signed candidate", stage_status == CBM_ACTIVATION_TRANSACTION_IO,
13367+
cbm_activation_transaction_status_message(stage_status), &failure) &&
13368+
cli_update_prep_step(
13369+
cli_activation_transaction_expected_build(binary_transaction, &staged_validator),
13370+
"fingerprinting the staged candidate", true, NULL, &failure) &&
13371+
cli_update_prep_step(strcmp(staged_validator.fingerprint, validator.fingerprint) == 0,
13372+
"comparing the staged and signed candidates", false,
13373+
"their fingerprints differ", &failure);
1329813374
if (prepared) {
1329913375
validator = staged_validator;
1330013376
}
@@ -13308,18 +13384,35 @@ static int extract_and_install_binary(extract_install_args_t args) {
1330813384
(void)cbm_rmdir(prepared_dir);
1330913385
}
1331013386
if (!prepared) {
13311-
(void)fprintf(stderr, "error: signed update candidate preparation failed\n");
13387+
cli_update_prep_report(&failure);
1331213388
(void)cli_activation_transaction_abort(&binary_transaction);
1331313389
return CLI_TRUE;
1331413390
}
1331513391
#else
13392+
errno = 0;
1331613393
stage_status = cbm_activation_transaction_stage_bytes(bin_dest, bin_data, (size_t)bin_len,
1331713394
&binary_transaction);
13395+
/* POSIX only: Windows staging reports through GetLastError, not errno. */
13396+
#ifdef _WIN32
13397+
int stage_error = 0;
13398+
#else
13399+
int stage_error = stage_status == CBM_ACTIVATION_TRANSACTION_IO ? errno : 0;
13400+
#endif
1331813401
free(bin_data);
13319-
if (stage_status != CBM_ACTIVATION_TRANSACTION_OK || !binary_transaction ||
13320-
!cli_activation_transaction_expected_build(binary_transaction, &validator)) {
13321-
(void)fprintf(stderr, "error: failed to stage verified update: %s\n",
13322-
cbm_activation_transaction_status_message(stage_status));
13402+
if (stage_status != CBM_ACTIVATION_TRANSACTION_OK || !binary_transaction) {
13403+
if (stage_error != 0) {
13404+
(void)fprintf(stderr, "error: failed to stage verified update: %s: %s\n",
13405+
cbm_activation_transaction_status_message(stage_status),
13406+
strerror(stage_error));
13407+
} else {
13408+
(void)fprintf(stderr, "error: failed to stage verified update: %s\n",
13409+
cbm_activation_transaction_status_message(stage_status));
13410+
}
13411+
(void)cli_activation_transaction_abort(&binary_transaction);
13412+
return CLI_TRUE;
13413+
}
13414+
if (!cli_activation_transaction_expected_build(binary_transaction, &validator)) {
13415+
(void)fprintf(stderr, "error: failed to fingerprint the staged update\n");
1332313416
(void)cli_activation_transaction_abort(&binary_transaction);
1332413417
return CLI_TRUE;
1332513418
}

0 commit comments

Comments
 (0)