From 7e812874394491003c3d2bbdcb73fdcff282c0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9C=9C=E5=86=BB=E5=85=AC=E7=88=B5?= Date: Fri, 16 Oct 2026 18:35:28 +0800 Subject: [PATCH] fix(cli): allow Windows project-lock preparation to complete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 霜冻公爵 --- src/daemon/project_lock.c | 6 +++ src/daemon/project_lock.h | 11 +++++ src/daemon/project_lock_internal.h | 11 +++++ src/main.c | 3 +- tests/test_project_lock.c | 73 ++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 src/daemon/project_lock_internal.h diff --git a/src/daemon/project_lock.c b/src/daemon/project_lock.c index 77548131f4..fc4ae8d0b5 100644 --- a/src/daemon/project_lock.c +++ b/src/daemon/project_lock.c @@ -1,5 +1,6 @@ /* project_lock.c — Shared daemon/local-CLI project mutation leases. */ #include "daemon/project_lock.h" +#include "daemon/project_lock_internal.h" #include #include @@ -20,6 +21,11 @@ struct cbm_project_lock_lease { cbm_lock_lease_t *project_set; }; +bool cbm_project_lock_set_stage_hook_for_test(cbm_project_lock_manager_t *manager, + cbm_lock_registry_stage_hook_fn hook, void *context) { + return manager && cbm_lock_registry_set_stage_hook_for_test(manager->registry, hook, context); +} + static bool project_lock_key(const char *project, char out[PROJECT_LOCK_KEY_CAP]) { if (!project || !project[0] || strcmp(project, "*") == 0) { return false; diff --git a/src/daemon/project_lock.h b/src/daemon/project_lock.h index cc65e919f6..f10320e1b8 100644 --- a/src/daemon/project_lock.h +++ b/src/daemon/project_lock.h @@ -10,6 +10,17 @@ typedef struct cbm_project_lock_manager cbm_project_lock_manager_t; typedef struct cbm_project_lock_lease cbm_project_lock_lease_t; +/* A CLI attempt validates several native lock files before it can publish a + * lease. Windows path/ACL validation can exceed a 100ms scheduling slice. */ +static inline uint64_t cbm_project_lock_cli_attempt_deadline(uint64_t now_ms) { +#ifdef _WIN32 + const uint64_t timeout_ms = 5000U; +#else + const uint64_t timeout_ms = 100U; +#endif + return now_ms > UINT64_MAX - timeout_ms ? UINT64_MAX : now_ms + timeout_ms; +} + /* Each manager is an independent process-local registry over the endpoint's * owner-only runtime directory. Separate CBM processes therefore coordinate * through the same native lock files without sharing memory. */ diff --git a/src/daemon/project_lock_internal.h b/src/daemon/project_lock_internal.h new file mode 100644 index 0000000000..869424104a --- /dev/null +++ b/src/daemon/project_lock_internal.h @@ -0,0 +1,11 @@ +#ifndef CBM_DAEMON_PROJECT_LOCK_INTERNAL_H +#define CBM_DAEMON_PROJECT_LOCK_INTERNAL_H + +#include "daemon/project_lock.h" +#include "foundation/lock_registry_internal.h" + +/* Forward the existing idle-only registry seam without exposing its handle. */ +bool cbm_project_lock_set_stage_hook_for_test(cbm_project_lock_manager_t *manager, + cbm_lock_registry_stage_hook_fn hook, void *context); + +#endif /* CBM_DAEMON_PROJECT_LOCK_INTERNAL_H */ diff --git a/src/main.c b/src/main.c index 54bad875eb..9249129b1e 100644 --- a/src/main.c +++ b/src/main.c @@ -317,8 +317,7 @@ static bool main_local_cli_mutation_begin_internal(void *context, const char *pr cbm_project_lock_lease_t *lease = NULL; cbm_private_file_lock_status_t status; if (wait) { - uint64_t now = cbm_now_ms(); - uint64_t deadline = now > UINT64_MAX - 100U ? UINT64_MAX : now + 100U; + uint64_t deadline = cbm_project_lock_cli_attempt_deadline(cbm_now_ms()); status = cbm_project_lock_acquire(mutation->manager, project, deadline, NULL, &lease); } else { status = cbm_project_lock_try_acquire(mutation->manager, project, &lease); diff --git a/tests/test_project_lock.c b/tests/test_project_lock.c index 52b406977f..c5b4a0de44 100644 --- a/tests/test_project_lock.c +++ b/tests/test_project_lock.c @@ -4,6 +4,7 @@ #include "daemon/ipc.h" #include "daemon/project_lock.h" +#include "daemon/project_lock_internal.h" #include "foundation/compat_fs.h" #include "foundation/platform.h" @@ -63,6 +64,78 @@ TEST(project_lock_coordinates_instances_projects_wildcard_and_case_aliases) { PASS(); } +#ifdef _WIN32 +static void project_lock_test_slow_native_preparation(void *context, + cbm_private_file_lock_mode_t mode, + cbm_lock_registry_stage_t stage) { + (void)mode; + if (stage == CBM_LOCK_REGISTRY_STAGE_TURN_HELD) { + (*(unsigned int *)context)++; + cbm_usleep(150000); + } +} +#endif + +TEST(project_lock_cli_attempt_survives_slow_windows_preparation) { +#ifdef _WIN32 + char runtime_parent[PROJECT_LOCK_TEST_PATH_CAP]; + (void)snprintf(runtime_parent, sizeof(runtime_parent), "%s/cbm-cli-lock-budget-XXXXXX", + cbm_tmpdir()); + ASSERT_NOT_NULL(cbm_mkdtemp(runtime_parent)); + cbm_daemon_ipc_endpoint_t *endpoint = + cbm_daemon_ipc_endpoint_new("9876543210abcdef", runtime_parent); + cbm_project_lock_manager_t *manager = cbm_project_lock_manager_new(endpoint); + cbm_project_lock_manager_t *competitor = cbm_project_lock_manager_new(endpoint); + unsigned int delayed_turns = 0; + bool ready = manager && competitor && + cbm_project_lock_set_stage_hook_for_test( + manager, project_lock_test_slow_native_preparation, &delayed_turns); + cbm_project_lock_lease_t *lease = NULL; + cbm_private_file_lock_status_t old_status = + ready ? cbm_project_lock_acquire(manager, "slow-preparation", cbm_now_ms() + 100U, NULL, + &lease) + : CBM_PRIVATE_FILE_LOCK_IO; + bool old_rolled_back = lease == NULL; + project_lock_test_release(&lease); + cbm_private_file_lock_status_t status = + ready ? cbm_project_lock_acquire(manager, "slow-preparation", + cbm_project_lock_cli_attempt_deadline(cbm_now_ms()), NULL, + &lease) + : CBM_PRIVATE_FILE_LOCK_IO; + bool acquired = status == CBM_PRIVATE_FILE_LOCK_OK && lease; + cbm_project_lock_lease_t *other = NULL; + cbm_private_file_lock_status_t blocked = + acquired ? cbm_project_lock_try_acquire(competitor, "slow-preparation", &other) + : CBM_PRIVATE_FILE_LOCK_IO; + bool competitor_rolled_back = other == NULL; + project_lock_test_release(&other); + project_lock_test_release(&lease); + cbm_private_file_lock_status_t released = + ready ? cbm_project_lock_try_acquire(competitor, "slow-preparation", &other) + : CBM_PRIVATE_FILE_LOCK_IO; + project_lock_test_release(&other); + cbm_private_file_lock_status_t competitor_freed = cbm_project_lock_manager_free(&competitor); + cbm_private_file_lock_status_t manager_freed = cbm_project_lock_manager_free(&manager); + cbm_daemon_ipc_endpoint_free(endpoint); + (void)th_rmtree(runtime_parent); + + ASSERT_TRUE(ready); + ASSERT_EQ(old_status, CBM_PRIVATE_FILE_LOCK_BUSY); + ASSERT_TRUE(old_rolled_back); + ASSERT_TRUE(delayed_turns >= 3U); + ASSERT_TRUE(acquired); + ASSERT_EQ(blocked, CBM_PRIVATE_FILE_LOCK_BUSY); + ASSERT_TRUE(competitor_rolled_back); + ASSERT_EQ(released, CBM_PRIVATE_FILE_LOCK_OK); + ASSERT_EQ(competitor_freed, CBM_PRIVATE_FILE_LOCK_OK); + ASSERT_EQ(manager_freed, CBM_PRIVATE_FILE_LOCK_OK); +#else + SKIP_PLATFORM("Windows CLI lock preparation budget"); +#endif + PASS(); +} + SUITE(project_lock) { RUN_TEST(project_lock_coordinates_instances_projects_wildcard_and_case_aliases); + RUN_TEST(project_lock_cli_attempt_survives_slow_windows_preparation); }