aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-07-12 01:27:46 +0900
committernsfisis <nsfisis@gmail.com>2026-07-16 01:02:47 +0900
commitba23ea4f22b5482d036706124a196fa03ef4b521 (patch)
treef743427e11f8ad7e08a041900f1e6aa5b67a7cd0
parente588920985c71eaa652a292db2dd1d75b73712e4 (diff)
downloadphp-shirabe-ba23ea4f22b5482d036706124a196fa03ef4b521.tar.gz
php-shirabe-ba23ea4f22b5482d036706124a196fa03ef4b521.tar.zst
php-shirabe-ba23ea4f22b5482d036706124a196fa03ef4b521.zip
fix(proxy-manager): correct singleton lifecycle and simplify to a plain Mutex
reset() eagerly rebuilt the ProxyManager singleton immediately, capturing env vars before a caller could set them for the next request. PHP's reset() just nulls the static instance; getInstance() lazily constructs on next use. Match that so proxy env vars set after reset() are observed. get_instance() also ensured the singleton was constructed under its own lock, dropped that lock, and returned the bare Mutex; every caller then took a second, independent lock. A reset() landing in that gap would leave the caller observing None and panicking on .as_ref().unwrap(), a state the old eager-reconstructing reset() could not produce. Return the already-locked MutexGuard from get_instance() instead, so construction and use happen under one lock, and update all call sites accordingly. Holding that guard across a loop body then deadlocked in diagnose_command, since check_http_proxy transitively re-enters get_instance() via HttpDownloader -> CurlDownloader, and std::sync::Mutex is not reentrant. Re-acquire the lock fresh each iteration with a short-lived guard instead. Finally, Mutex::new is a const fn, so the OnceLock wrapper around it was unnecessary indirection; a bare static Mutex<Option<ProxyManager>> initializes to the same state without the get_or_init/get dance. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-rw-r--r--crates/shirabe/src/command/diagnose_command.rs9
-rw-r--r--crates/shirabe/src/util/http/curl_downloader.rs2
-rw-r--r--crates/shirabe/src/util/http/proxy_manager.rs25
-rw-r--r--crates/shirabe/src/util/remote_filesystem.rs2
-rw-r--r--crates/shirabe/src/util/stream_context_factory.rs2
-rw-r--r--crates/shirabe/tests/util/http/proxy_manager_test.rs27
6 files changed, 34 insertions, 33 deletions
diff --git a/crates/shirabe/src/command/diagnose_command.rs b/crates/shirabe/src/command/diagnose_command.rs
index e7fb9c74..e457fbb2 100644
--- a/crates/shirabe/src/command/diagnose_command.rs
+++ b/crates/shirabe/src/command/diagnose_command.rs
@@ -338,7 +338,6 @@ impl Command for DiagnoseCommand {
}
}
- let proxy_manager = ProxyManager::get_instance();
let protos: Vec<&str> = if config.borrow_mut().get("disable-tls").as_bool() == Some(true) {
vec!["http"]
} else {
@@ -346,9 +345,11 @@ impl Command for DiagnoseCommand {
};
let proxy_check_result: anyhow::Result<(), anyhow::Error> = (|| -> anyhow::Result<()> {
for proto in &protos {
- let proxy = proxy_manager
- .lock()
- .unwrap()
+ // Compute the proxy under a short-lived lock: `check_http_proxy` below transitively
+ // re-enters `ProxyManager::get_instance()` (via HttpDownloader -> CurlDownloader /
+ // RemoteFilesystem), and `std::sync::Mutex` is not reentrant, so the guard must not
+ // still be held when that call happens.
+ let proxy = ProxyManager::get_instance()
.as_ref()
.unwrap()
.get_proxy_for_request(&format!("{}://repo.packagist.org", proto))
diff --git a/crates/shirabe/src/util/http/curl_downloader.rs b/crates/shirabe/src/util/http/curl_downloader.rs
index 6a6a1d66..f698fc25 100644
--- a/crates/shirabe/src/util/http/curl_downloader.rs
+++ b/crates/shirabe/src/util/http/curl_downloader.rs
@@ -222,8 +222,6 @@ impl CurlDownloader {
// PHP logs the proxy in the "Downloading" line; resolving it here keeps that message
// faithful even though reqwest does not yet apply the proxy (see send_once TODO).
let using_proxy = ProxyManager::get_instance()
- .lock()
- .unwrap()
.as_ref()
.map(|pm| pm.get_proxy_for_request(url))
.transpose()
diff --git a/crates/shirabe/src/util/http/proxy_manager.rs b/crates/shirabe/src/util/http/proxy_manager.rs
index 3bc0888d..82e8ebc5 100644
--- a/crates/shirabe/src/util/http/proxy_manager.rs
+++ b/crates/shirabe/src/util/http/proxy_manager.rs
@@ -4,10 +4,10 @@ use crate::downloader::TransportException;
use crate::util::NoProxyPattern;
use crate::util::http::ProxyItem;
use crate::util::http::RequestProxy;
+use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
-use std::sync::{Mutex, OnceLock};
-static INSTANCE: OnceLock<Mutex<Option<ProxyManager>>> = OnceLock::new();
+static INSTANCE: Mutex<Option<ProxyManager>> = Mutex::new(None);
// Distinguishes ProxyManager instances so tests can mirror PHP `===` identity of the singleton,
// which the Rust value-based singleton does not otherwise expose.
@@ -37,14 +37,25 @@ impl ProxyManager {
instance
}
- pub fn get_instance() -> &'static Mutex<Option<ProxyManager>> {
- INSTANCE.get_or_init(|| Mutex::new(Some(ProxyManager::new())))
+ // Returns the singleton already locked, rather than the bare `Mutex`, so that ensuring the
+ // instance is constructed and reading it happen under a single lock. Returning the `Mutex`
+ // itself would let a caller's own separate `.lock()` race a concurrent `reset()` in the gap
+ // between the two locks.
+ pub fn get_instance() -> std::sync::MutexGuard<'static, Option<ProxyManager>> {
+ // Mirrors PHP `getInstance`'s `if (self::$instance === null) { self::$instance = new self(); }`:
+ // construction is lazy, so it observes the environment at first use after a `reset`
+ // rather than at `reset` time.
+ let mut guard = INSTANCE.lock().unwrap();
+ if guard.is_none() {
+ *guard = Some(ProxyManager::new());
+ }
+ guard
}
+ /// Clears the persistent instance (mirrors PHP `ProxyManager::reset`, which sets
+ /// `self::$instance = null` rather than eagerly reconstructing it).
pub fn reset() {
- if let Some(mutex) = INSTANCE.get() {
- *mutex.lock().unwrap() = Some(ProxyManager::new());
- }
+ *INSTANCE.lock().unwrap() = None;
}
/// For testing only: a unique id per constructed instance, used to mirror PHP `===` identity
diff --git a/crates/shirabe/src/util/remote_filesystem.rs b/crates/shirabe/src/util/remote_filesystem.rs
index fc61b16f..9b851007 100644
--- a/crates/shirabe/src/util/remote_filesystem.rs
+++ b/crates/shirabe/src/util/remote_filesystem.rs
@@ -256,7 +256,7 @@ impl RemoteFilesystem {
.map_err(|e| anyhow::anyhow!(e))?;
let using_proxy = {
- let proxy_manager_guard = ProxyManager::get_instance().lock().unwrap();
+ let proxy_manager_guard = ProxyManager::get_instance();
let proxy = proxy_manager_guard
.as_ref()
.expect("ProxyManager instance")
diff --git a/crates/shirabe/src/util/stream_context_factory.rs b/crates/shirabe/src/util/stream_context_factory.rs
index 43357772..cae00c08 100644
--- a/crates/shirabe/src/util/stream_context_factory.rs
+++ b/crates/shirabe/src/util/stream_context_factory.rs
@@ -96,7 +96,7 @@ impl StreamContextFactory {
// Add stream proxy options if there is a proxy
if !for_curl {
- let proxy_manager = ProxyManager::get_instance().lock().unwrap();
+ let proxy_manager = ProxyManager::get_instance();
let proxy_manager = proxy_manager.as_ref().unwrap();
let proxy = proxy_manager.get_proxy_for_request(url)?;
let proxy_options = proxy.get_context_options();
diff --git a/crates/shirabe/tests/util/http/proxy_manager_test.rs b/crates/shirabe/tests/util/http/proxy_manager_test.rs
index 7beafe19..d37a1fd8 100644
--- a/crates/shirabe/tests/util/http/proxy_manager_test.rs
+++ b/crates/shirabe/tests/util/http/proxy_manager_test.rs
@@ -46,21 +46,18 @@ fn test_instantiation() {
// PHP compares object identity (===); the value-based Rust singleton exposes a per-instance
// generation id instead, which changes only when a new ProxyManager is constructed.
let original_instance = {
- let mutex = ProxyManager::get_instance();
- let guard = mutex.lock().unwrap();
+ let guard = ProxyManager::get_instance();
guard.as_ref().unwrap().__generation()
};
let same_instance = {
- let mutex = ProxyManager::get_instance();
- let guard = mutex.lock().unwrap();
+ let guard = ProxyManager::get_instance();
guard.as_ref().unwrap().__generation()
};
assert_eq!(original_instance, same_instance);
ProxyManager::reset();
let new_instance = {
- let mutex = ProxyManager::get_instance();
- let guard = mutex.lock().unwrap();
+ let guard = ProxyManager::get_instance();
guard.as_ref().unwrap().__generation()
};
assert_ne!(same_instance, new_instance);
@@ -74,8 +71,7 @@ fn test_get_proxy_for_request_throws_on_bad_proxy_url() {
Platform::put_env("http_proxy", "localhost");
ProxyManager::reset();
- let mutex = ProxyManager::get_instance();
- let guard = mutex.lock().unwrap();
+ let guard = ProxyManager::get_instance();
let proxy_manager = guard.as_ref().unwrap();
assert!(
@@ -126,8 +122,7 @@ fn test_lowercase_overrides_uppercase() {
}
ProxyManager::reset();
- let mutex = ProxyManager::get_instance();
- let guard = mutex.lock().unwrap();
+ let guard = ProxyManager::get_instance();
let proxy = guard.as_ref().unwrap().get_proxy_for_request(url).unwrap();
assert_eq!(expected_url, proxy.get_status(None).unwrap());
}
@@ -161,8 +156,7 @@ fn test_cgi_proxy_is_only_used_when_no_http_proxy() {
}
ProxyManager::reset();
- let mutex = ProxyManager::get_instance();
- let guard = mutex.lock().unwrap();
+ let guard = ProxyManager::get_instance();
let proxy = guard
.as_ref()
.unwrap()
@@ -180,8 +174,7 @@ fn test_no_http_proxy_does_not_use_https_proxy() {
Platform::put_env("https_proxy", "https://proxy.com:443");
ProxyManager::reset();
- let mutex = ProxyManager::get_instance();
- let guard = mutex.lock().unwrap();
+ let guard = ProxyManager::get_instance();
let proxy = guard
.as_ref()
.unwrap()
@@ -198,8 +191,7 @@ fn test_no_https_proxy_does_not_use_http_proxy() {
Platform::put_env("http_proxy", "http://proxy.com:80");
ProxyManager::reset();
- let mutex = ProxyManager::get_instance();
- let guard = mutex.lock().unwrap();
+ let guard = ProxyManager::get_instance();
let proxy = guard
.as_ref()
.unwrap()
@@ -280,8 +272,7 @@ fn test_get_proxy_for_request() {
}
ProxyManager::reset();
- let mutex = ProxyManager::get_instance();
- let guard = mutex.lock().unwrap();
+ let guard = ProxyManager::get_instance();
let proxy = guard.as_ref().unwrap().get_proxy_for_request(url).unwrap();
assert_eq!(options.as_ref(), proxy.get_context_options());