feat: 完成 OpenNexus 第三阶段核心功能与生产化基础 #45
@@ -419,3 +419,13 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写
|
||||
- 实际 Stronghold 测试验证 Provider 同名凭据不可见、正确 MCP 包域可用、重新签发后改变类型/来源/命名空间/包 ID 仍不能读取原凭据,以及入口/树/Vault/策略不匹配、参数篡改、过期、锁定和编码错误拒绝。真实 AppContainer 原生探针通过该准备链取得虚构测试凭据后启动,完整 argv/环境核对通过,未使用任何用户真实秘密。
|
||||
- 52 项扩展回归通过,4 项 ignored 为三个由父测试驱动的 Job 辅助入口和单独执行的 60 秒验收;全目标 Clippy -D warnings 通过。日志 `.build/extension-launch-authorization-tests.log`、`.build/extension-launch-authorization-clippy.log`。
|
||||
- 该组件只完成准备阶段,不是可直接恢复执行的授权租约;调用方仍须在 resume 前复核在线信任、活动安装身份、许可、Vault 和锁定代次,并完成运行中撤销/停止、broker 和完整资源限制。目前未开放第三方执行,整体生产化继续进行。
|
||||
|
||||
|
||||
## 增量:运行租约、锁定代次与主动进程树撤销
|
||||
|
||||
- 执行许可更新为内存态 v2 HMAC,包含签发代次;Authority.invalidate_all 与 Authority 释放都会推进代次。Lease 绑定原许可代次及由有效期计算的单调时钟期限,不能把撤销后的当前代次重新当作旧许可的有效代次。
|
||||
- Context.prepare 在解析凭据前捕获租约与保险库锁定代次,生成不可拆换的 PreparedLaunch;创建挂起进程前再次核对租约、绑定路径、相对入口和树摘要,创建后继续核对。LeasedSuspended 恢复时先建立原生撤销监视器,进程生命周期继续保留入口与 Profile 借用。
|
||||
- 监视器每 50 ms 检查许可代次、凭据代次与到期时间;任何失效均终止所属 Job,不要求调用者继续轮询。监视线程退出也有整组终止兜底;监视器不可用时不恢复执行。Running.start_tool_call 先检查租约,阻止已撤销实例启动新工具调用。
|
||||
- 真实 AppContainer 双进程探针验证:本轮许可作废后约 56.90 ms、保险库锁定后约 47.80 ms、签发器释放后约 55.45 ms 观察到进程树清空;之后新工具调用均被拒绝。两秒测试租约自然到期也主动清空进程树。创建前撤销和创建后恢复前撤销均拒绝继续,无普通子进程降级。
|
||||
- 原生参数/环境探针已使用 prepare → create_suspended → 带租约恢复路径。52 项扩展回归通过,4 项 ignored 为三个父测试实际驱动的 Job 辅助入口及单独执行的 60 秒验收;全目标 Clippy -D warnings 通过。日志 `.build/extension-revocation-tests.log`、`.build/extension-revocation-clippy.log`。
|
||||
- 本轮锁定由真实 CredentialBroker.lock 触发,尚不是操作系统锁屏事件的桌面端到端测试。监视器提供有界撤销,并非对核验与 ResumeThread 之间竞态作“零指令执行”证明。Host 仍需把实际 Vault 切换、权限撤回、在线签名撤销等事件接入签发器/实例注册表,并完成 broker 与全部资源策略,整体生产化尚未完成。
|
||||
|
||||
@@ -764,17 +764,98 @@ mod tests {
|
||||
container_data: &folder,
|
||||
scratch: &folder.join("Temp"),
|
||||
};
|
||||
let data = context
|
||||
.build(&authority, &permit, &claims, &bound_entry, &broker, 2)
|
||||
let prepared = context
|
||||
.prepare(&authority, &permit, &claims, &bound_entry, &broker, 2)
|
||||
.unwrap();
|
||||
let suspended =
|
||||
crate::extension_process::Suspended::create_bound(&profile, &bound_entry, data)
|
||||
.unwrap();
|
||||
let suspended = prepared.create_suspended(&profile, &bound_entry).unwrap();
|
||||
let running = unsafe { suspended.resume().unwrap() };
|
||||
assert_eq!(
|
||||
running.wait(std::time::Duration::from_secs(5)).unwrap(),
|
||||
Some(0)
|
||||
);
|
||||
drop(running);
|
||||
for cause in [
|
||||
"before_create",
|
||||
"before_resume",
|
||||
"permit",
|
||||
"credential",
|
||||
"owner_drop",
|
||||
"expiry",
|
||||
] {
|
||||
let mut issuer = Authority::default();
|
||||
let mut waiting = claims.clone();
|
||||
waiting.arguments = vec!["wait_tree".into()];
|
||||
waiting.expires_at_ms = if cause == "expiry" { 2_002 } else { 120_000 };
|
||||
let permit = issuer.issue(&waiting, 1).unwrap();
|
||||
let prepared = context
|
||||
.prepare(&issuer, &permit, &waiting, &bound_entry, &broker, 2)
|
||||
.unwrap();
|
||||
if cause == "before_create" {
|
||||
issuer.invalidate_all();
|
||||
assert_eq!(
|
||||
prepared
|
||||
.create_suspended(&profile, &bound_entry)
|
||||
.err()
|
||||
.unwrap()
|
||||
.code,
|
||||
"EXTENSION_PERMIT_REVOKED"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let suspended = prepared.create_suspended(&profile, &bound_entry).unwrap();
|
||||
if cause == "before_resume" {
|
||||
issuer.invalidate_all();
|
||||
assert_eq!(
|
||||
unsafe { suspended.resume() }.err().unwrap().code,
|
||||
"EXTENSION_PERMIT_REVOKED"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let running = unsafe { suspended.resume().unwrap() };
|
||||
let started = std::time::Instant::now();
|
||||
while running.active_test_processes().unwrap() != 2
|
||||
&& started.elapsed() < std::time::Duration::from_secs(5)
|
||||
{
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
assert_eq!(running.active_test_processes().unwrap(), 2);
|
||||
let revoked = std::time::Instant::now();
|
||||
match cause {
|
||||
"permit" => issuer.invalidate_all(),
|
||||
"credential" => broker.lock(),
|
||||
"expiry" => {}
|
||||
_ => drop(issuer),
|
||||
}
|
||||
// The monitor must act without check_authorization or tool polling.
|
||||
assert!(running
|
||||
.wait(std::time::Duration::from_secs(5))
|
||||
.unwrap()
|
||||
.is_some());
|
||||
while running.active_test_processes().unwrap() != 0
|
||||
&& revoked.elapsed() < std::time::Duration::from_secs(5)
|
||||
{
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
assert_eq!(running.active_test_processes().unwrap(), 0);
|
||||
assert!(revoked.elapsed() < std::time::Duration::from_secs(5));
|
||||
let expected = match cause {
|
||||
"credential" => "CREDENTIALS_LOCKED",
|
||||
"expiry" => "EXTENSION_PERMIT_EXPIRED",
|
||||
_ => "EXTENSION_PERMIT_REVOKED",
|
||||
};
|
||||
assert_eq!(running.check_authorization().unwrap_err().code, expected);
|
||||
assert_eq!(running.start_tool_call().err().unwrap().code, expected);
|
||||
eprintln!(
|
||||
"instance revocation {cause}: tree empty after {:?}",
|
||||
revoked.elapsed()
|
||||
);
|
||||
drop(running);
|
||||
if cause == "credential" {
|
||||
broker
|
||||
.unlock(Zeroizing::new(b"native fixture passphrase".to_vec()))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
assert_eq!(
|
||||
|
||||
@@ -19,6 +19,46 @@ pub struct Context<'a> {
|
||||
pub container_data: &'a Path,
|
||||
pub scratch: &'a Path,
|
||||
}
|
||||
pub struct PreparedLaunch {
|
||||
data: LaunchData,
|
||||
lease: crate::extension_permit::Lease,
|
||||
path: std::path::PathBuf,
|
||||
entry: String,
|
||||
tree: String,
|
||||
}
|
||||
pub struct LeasedSuspended<'a> {
|
||||
process: crate::extension_process::Suspended<'a>,
|
||||
lease: crate::extension_permit::Lease,
|
||||
}
|
||||
impl PreparedLaunch {
|
||||
pub fn create_suspended<'a>(
|
||||
self,
|
||||
profile: &'a crate::extension_container::Profile,
|
||||
entry: &'a BoundEntry<'a>,
|
||||
) -> Result<LeasedSuspended<'a>> {
|
||||
self.lease.check()?;
|
||||
if self.path != entry.path()
|
||||
|| self.entry != entry.relative_name()
|
||||
|| self.tree != entry.tree_sha256()
|
||||
{
|
||||
return Err(HostError::new("EXTENSION_ENTRY_PERMIT_MISMATCH"));
|
||||
}
|
||||
let process = crate::extension_process::Suspended::create_bound(profile, entry, self.data)?;
|
||||
self.lease.check()?;
|
||||
Ok(LeasedSuspended {
|
||||
process,
|
||||
lease: self.lease,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl<'a> LeasedSuspended<'a> {
|
||||
/// # Safety
|
||||
/// Live trust, active installation, broker and all sandbox resource policy
|
||||
/// requirements must also hold. A lease does not establish those conditions.
|
||||
pub unsafe fn resume(self) -> Result<crate::extension_process::Running<'a>> {
|
||||
unsafe { self.process.resume_with_lease(self.lease) }
|
||||
}
|
||||
}
|
||||
struct EnvironmentValues(BTreeMap<String, String>);
|
||||
impl Drop for EnvironmentValues {
|
||||
fn drop(&mut self) {
|
||||
@@ -66,6 +106,35 @@ pub fn credential_id(claims: &Claims, reference: &str) -> Result<CredentialId> {
|
||||
})
|
||||
}
|
||||
impl Context<'_> {
|
||||
/// Capture epochs before resolving credentials; never adopt a newer lock
|
||||
/// generation for launch bytes prepared under an earlier session.
|
||||
pub fn prepare(
|
||||
&self,
|
||||
authority: &Authority,
|
||||
permit: &Permit,
|
||||
claims: &Claims,
|
||||
entry: &BoundEntry<'_>,
|
||||
broker: &CredentialBroker,
|
||||
now_ms: u64,
|
||||
) -> Result<PreparedLaunch> {
|
||||
let mut lease = authority.lease(permit, claims, now_ms)?;
|
||||
if claims
|
||||
.environment
|
||||
.values()
|
||||
.any(|v| matches!(v, Environment::CredentialScope(_)))
|
||||
{
|
||||
lease.bind_credential(broker.lock_signal());
|
||||
}
|
||||
let data = self.build(authority, permit, claims, entry, broker, now_ms)?;
|
||||
lease.check()?;
|
||||
Ok(PreparedLaunch {
|
||||
data,
|
||||
lease,
|
||||
path: entry.path().to_owned(),
|
||||
entry: entry.relative_name().to_owned(),
|
||||
tree: entry.tree_sha256().to_owned(),
|
||||
})
|
||||
}
|
||||
/// Caller must still recheck live trust/permit/session state immediately
|
||||
/// before resume; returning encoded data is not an execution lease.
|
||||
pub fn build(
|
||||
|
||||
@@ -6,6 +6,13 @@ use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -46,12 +53,46 @@ pub struct Claims {
|
||||
|
||||
/// Opaque authenticator; the Host retains claims separately. No paths, arguments
|
||||
/// or credential declarations need to be passed to a renderer with the token.
|
||||
pub struct Permit([u8; 32]);
|
||||
pub struct Permit {
|
||||
mac: [u8; 32],
|
||||
generation: u64,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct Lease {
|
||||
generation: (Arc<AtomicU64>, u64),
|
||||
credential: Option<(Arc<AtomicU64>, u64)>,
|
||||
expires: Instant,
|
||||
}
|
||||
impl Lease {
|
||||
pub fn check(&self) -> Result<()> {
|
||||
if self.generation.0.load(Ordering::SeqCst) != self.generation.1 {
|
||||
return Err(HostError::new("EXTENSION_PERMIT_REVOKED"));
|
||||
}
|
||||
if self
|
||||
.credential
|
||||
.as_ref()
|
||||
.is_some_and(|(signal, epoch)| signal.load(Ordering::SeqCst) != *epoch)
|
||||
{
|
||||
return Err(HostError::new("CREDENTIALS_LOCKED"));
|
||||
}
|
||||
if Instant::now() >= self.expires {
|
||||
return Err(HostError::new("EXTENSION_PERMIT_EXPIRED"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn bind_credential(&mut self, signal: Arc<AtomicU64>) {
|
||||
let epoch = signal.load(Ordering::SeqCst);
|
||||
self.credential = Some((signal, epoch));
|
||||
}
|
||||
}
|
||||
pub struct Authority {
|
||||
key: [u8; 32],
|
||||
generation: Arc<AtomicU64>,
|
||||
}
|
||||
impl Drop for Authority {
|
||||
fn drop(&mut self) {
|
||||
self.generation.fetch_add(1, Ordering::SeqCst);
|
||||
self.key.zeroize();
|
||||
}
|
||||
}
|
||||
@@ -59,7 +100,10 @@ impl Default for Authority {
|
||||
fn default() -> Self {
|
||||
let mut key = [0; 32];
|
||||
rand::rngs::OsRng.fill_bytes(&mut key);
|
||||
Self { key }
|
||||
Self {
|
||||
key,
|
||||
generation: Arc::new(AtomicU64::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Claims {
|
||||
@@ -157,23 +201,52 @@ impl Authority {
|
||||
/// Call only after consent and live trust validation. This authenticates the
|
||||
/// decision; it does not establish sandbox availability or grant broker access.
|
||||
pub fn issue(&self, claims: &Claims, now_ms: u64) -> Result<Permit> {
|
||||
let generation = self.generation.load(Ordering::SeqCst);
|
||||
let encoded = claims.encoded(now_ms)?;
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("HMAC key");
|
||||
mac.update(b"OpenNexus execution permit v1\0");
|
||||
mac.update(b"OpenNexus execution permit v2\0");
|
||||
mac.update(&generation.to_be_bytes());
|
||||
mac.update(&encoded);
|
||||
Ok(Permit(mac.finalize().into_bytes().into()))
|
||||
Ok(Permit {
|
||||
mac: mac.finalize().into_bytes().into(),
|
||||
generation,
|
||||
})
|
||||
}
|
||||
pub fn verify(&self, permit: &Permit, actual: &Claims, now_ms: u64) -> Result<()> {
|
||||
let generation = self.generation.load(Ordering::SeqCst);
|
||||
if generation != permit.generation {
|
||||
return Err(HostError::new("EXTENSION_PERMIT_REVOKED"));
|
||||
}
|
||||
let encoded = actual.encoded(now_ms)?;
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("HMAC key");
|
||||
mac.update(b"OpenNexus execution permit v1\0");
|
||||
mac.update(b"OpenNexus execution permit v2\0");
|
||||
mac.update(&generation.to_be_bytes());
|
||||
mac.update(&encoded);
|
||||
mac.verify_slice(&permit.0)
|
||||
.map_err(|_| HostError::new("EXTENSION_PERMIT_MISMATCH"))
|
||||
mac.verify_slice(&permit.mac)
|
||||
.map_err(|_| HostError::new("EXTENSION_PERMIT_MISMATCH"))?;
|
||||
if generation != self.generation.load(Ordering::SeqCst) {
|
||||
return Err(HostError::new("EXTENSION_PERMIT_REVOKED"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn lease(&self, permit: &Permit, claims: &Claims, now_ms: u64) -> Result<Lease> {
|
||||
let started = Instant::now();
|
||||
self.verify(permit, claims, now_ms)?;
|
||||
let expires = started
|
||||
.checked_add(Duration::from_millis(claims.expires_at_ms - now_ms))
|
||||
.ok_or_else(|| HostError::new("EXTENSION_PERMIT_INVALID"))?;
|
||||
let lease = Lease {
|
||||
generation: (Arc::clone(&self.generation), permit.generation),
|
||||
credential: None,
|
||||
expires,
|
||||
};
|
||||
lease.check()?;
|
||||
Ok(lease)
|
||||
}
|
||||
/// Lock/logout/policy invalidation may discard all permits. Restart creates a
|
||||
/// fresh key, so an old process token cannot silently revive authorization.
|
||||
pub fn invalidate_all(&mut self) {
|
||||
self.generation.fetch_add(1, Ordering::SeqCst);
|
||||
self.key.zeroize();
|
||||
rand::rngs::OsRng.fill_bytes(&mut self.key);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,11 @@ impl Drop for Process<'_> {
|
||||
}
|
||||
}
|
||||
pub struct Suspended<'a>(Process<'a>);
|
||||
pub struct Running<'a>(Process<'a>);
|
||||
pub struct Running<'a> {
|
||||
process: Process<'a>,
|
||||
#[cfg(feature = "desktop")]
|
||||
revocation: Option<crate::extension_revocation::Watch>,
|
||||
}
|
||||
impl<'a> Suspended<'a> {
|
||||
/// Creates hidden, with no inherited handles and an explicit environment and
|
||||
/// current directory. The profile borrow prevents cleanup while this owner
|
||||
@@ -187,25 +191,57 @@ impl<'a> Suspended<'a> {
|
||||
if unsafe { ResumeThread(self.0.handles.thread.as_raw_handle()) } != 1 {
|
||||
return Err(HostError::new("EXTENSION_PROCESS_RESUME_FAILED"));
|
||||
}
|
||||
Ok(Running(self.0))
|
||||
Ok(Running {
|
||||
process: self.0,
|
||||
#[cfg(feature = "desktop")]
|
||||
revocation: None,
|
||||
})
|
||||
}
|
||||
/// # Safety
|
||||
/// The same complete resource/broker/trust preconditions as resume apply.
|
||||
/// This additionally arms revocation monitoring before any instruction resumes.
|
||||
#[cfg(feature = "desktop")]
|
||||
pub unsafe fn resume_with_lease(
|
||||
self,
|
||||
lease: crate::extension_permit::Lease,
|
||||
) -> Result<Running<'a>> {
|
||||
let watch = crate::extension_revocation::Watch::arm(&self.0.job, lease)?;
|
||||
watch.check()?;
|
||||
if unsafe { ResumeThread(self.0.handles.thread.as_raw_handle()) } != 1 {
|
||||
return Err(HostError::new("EXTENSION_PROCESS_RESUME_FAILED"));
|
||||
}
|
||||
let running = Running {
|
||||
process: self.0,
|
||||
revocation: Some(watch),
|
||||
};
|
||||
running.check_authorization()?;
|
||||
Ok(running)
|
||||
}
|
||||
}
|
||||
impl Running<'_> {
|
||||
pub fn check_authorization(&self) -> Result<()> {
|
||||
#[cfg(feature = "desktop")]
|
||||
if let Some(watch) = &self.revocation {
|
||||
return watch.check();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(crate) fn active_test_processes(&self) -> Result<u32> {
|
||||
self.0.job.active_processes()
|
||||
self.process.job.active_processes()
|
||||
}
|
||||
/// Arm before dispatching a tool request; finish after receiving its result.
|
||||
/// Failure to arm must prevent dispatch. This does not time server lifetime.
|
||||
pub fn start_tool_call(&self) -> Result<crate::extension_deadline::ToolDeadline> {
|
||||
crate::extension_deadline::ToolDeadline::arm(&self.0.job)
|
||||
self.check_authorization()?;
|
||||
crate::extension_deadline::ToolDeadline::arm(&self.process.job)
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(crate) fn start_test_tool_call(
|
||||
&self,
|
||||
budget: Duration,
|
||||
) -> Result<crate::extension_deadline::ToolDeadline> {
|
||||
crate::extension_deadline::ToolDeadline::arm_test(&self.0.job, budget)
|
||||
crate::extension_deadline::ToolDeadline::arm_test(&self.process.job, budget)
|
||||
}
|
||||
/// A bounded observation only. The runtime must enforce the tool deadline.
|
||||
pub fn wait(&self, timeout: Duration) -> Result<Option<u32>> {
|
||||
@@ -213,12 +249,15 @@ impl Running<'_> {
|
||||
.ok()
|
||||
.filter(|n| *n <= 60000)
|
||||
.ok_or_else(|| HostError::new("EXTENSION_PROCESS_WAIT_INVALID"))?;
|
||||
match unsafe { WaitForSingleObject(self.0.handles.process.as_raw_handle(), milliseconds) } {
|
||||
match unsafe {
|
||||
WaitForSingleObject(self.process.handles.process.as_raw_handle(), milliseconds)
|
||||
} {
|
||||
WAIT_TIMEOUT => Ok(None),
|
||||
WAIT_OBJECT_0 => {
|
||||
let mut code = 0;
|
||||
if unsafe { GetExitCodeProcess(self.0.handles.process.as_raw_handle(), &mut code) }
|
||||
== 0
|
||||
if unsafe {
|
||||
GetExitCodeProcess(self.process.handles.process.as_raw_handle(), &mut code)
|
||||
} == 0
|
||||
{
|
||||
return Err(HostError::new("EXTENSION_PROCESS_QUERY_FAILED"));
|
||||
}
|
||||
@@ -229,7 +268,7 @@ impl Running<'_> {
|
||||
}
|
||||
/// Terminates the entire managed group, including descendants.
|
||||
pub fn terminate(&self) -> Result<()> {
|
||||
self.0.job.terminate()
|
||||
self.process.job.terminate()
|
||||
}
|
||||
}
|
||||
fn verify_identity(handles: &Handles, profile: &Profile) -> Result<()> {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Native instance monitor; revocation does not depend on the caller polling.
|
||||
use crate::{
|
||||
extension_job::Job,
|
||||
extension_permit::Lease,
|
||||
workspace::{HostError, Result},
|
||||
};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Condvar, Mutex,
|
||||
},
|
||||
thread::JoinHandle,
|
||||
time::Duration,
|
||||
};
|
||||
struct KillOnExit(Job);
|
||||
impl Drop for KillOnExit {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.0.terminate();
|
||||
}
|
||||
}
|
||||
pub(crate) struct Watch {
|
||||
lease: Lease,
|
||||
cancelled: Arc<(Mutex<bool>, Condvar)>,
|
||||
failed: Arc<AtomicBool>,
|
||||
worker: Option<JoinHandle<()>>,
|
||||
}
|
||||
impl Watch {
|
||||
pub(crate) fn arm(job: &Job, lease: Lease) -> Result<Self> {
|
||||
lease.check()?;
|
||||
let job = KillOnExit(job.clone_for_deadline()?);
|
||||
let cancelled = Arc::new((Mutex::new(false), Condvar::new()));
|
||||
let failed = Arc::new(AtomicBool::new(false));
|
||||
let thread_cancel = Arc::clone(&cancelled);
|
||||
let thread_failed = Arc::clone(&failed);
|
||||
let thread_lease = lease.clone();
|
||||
let worker = std::thread::Builder::new()
|
||||
.name("extension-revocation".into())
|
||||
.spawn(move || {
|
||||
let mut stop = thread_cancel.0.lock().unwrap_or_else(|e| e.into_inner());
|
||||
while !*stop {
|
||||
if thread_lease.check().is_err() {
|
||||
if job.0.terminate().is_err() {
|
||||
thread_failed.store(true, Ordering::Release);
|
||||
}
|
||||
return;
|
||||
}
|
||||
stop = thread_cancel
|
||||
.1
|
||||
.wait_timeout(stop, Duration::from_millis(50))
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.0;
|
||||
}
|
||||
})
|
||||
.map_err(|_| HostError::new("EXTENSION_REVOCATION_WATCH_UNAVAILABLE"))?;
|
||||
Ok(Self {
|
||||
lease,
|
||||
cancelled,
|
||||
failed,
|
||||
worker: Some(worker),
|
||||
})
|
||||
}
|
||||
pub(crate) fn check(&self) -> Result<()> {
|
||||
if self.failed.load(Ordering::Acquire) {
|
||||
return Err(HostError::new("EXTENSION_RESOURCE_TERMINATE_FAILED"));
|
||||
}
|
||||
self.lease.check()?;
|
||||
if self
|
||||
.worker
|
||||
.as_ref()
|
||||
.is_some_and(|worker| worker.is_finished())
|
||||
{
|
||||
return Err(HostError::new("EXTENSION_REVOCATION_WATCH_FAILED"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Drop for Watch {
|
||||
fn drop(&mut self) {
|
||||
*self.cancelled.0.lock().unwrap_or_else(|e| e.into_inner()) = true;
|
||||
self.cancelled.1.notify_all();
|
||||
if let Some(worker) = self.worker.take() {
|
||||
let _ = worker.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,3 +72,6 @@ pub mod extension_pinned;
|
||||
|
||||
#[cfg(all(windows, feature = "desktop"))]
|
||||
pub mod extension_launch_authorization;
|
||||
|
||||
#[cfg(all(windows, feature = "desktop"))]
|
||||
mod extension_revocation;
|
||||
|
||||
Reference in New Issue
Block a user