1use std::env;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use std::time::Duration;
11
12use lean_rs::{LeanBuiltCapability, LeanCapabilityPreflight, LeanLoaderDiagnosticCode};
13use serde::Deserialize;
14use serde_json::Value;
15
16use crate::pool::{LeanWorkerRestartPolicyClass, LeanWorkerSessionKey};
17use crate::session::{
18 LeanWorkerCancellationToken, LeanWorkerCapabilityMetadata, LeanWorkerProgressSink, LeanWorkerRuntimeMetadata,
19 LeanWorkerSession, LeanWorkerSessionConfig,
20};
21use crate::supervisor::{
22 LEAN_WORKER_REQUEST_TIMEOUT_LONG_RUNNING, LeanWorker, LeanWorkerConfig, LeanWorkerError, LeanWorkerRestartPolicy,
23};
24
25const WORKER_CHILD_ENV: &str = "LEAN_RS_WORKER_CHILD";
26
27#[derive(Clone, Debug)]
42pub struct LeanWorkerCapabilityBuilder {
43 project_root: PathBuf,
44 package: String,
45 lib_name: String,
46 imports: Vec<String>,
47 built_dylib_path: Option<PathBuf>,
48 built_capability: Option<LeanBuiltCapability>,
49 worker_child: Option<LeanWorkerChild>,
50 startup_timeout: Option<Duration>,
51 request_timeout: Option<Duration>,
52 restart_policy: Option<LeanWorkerRestartPolicy>,
53 metadata_check: Option<CapabilityMetadataCheck>,
54}
55
56impl LeanWorkerCapabilityBuilder {
57 #[must_use]
63 pub fn new(
64 project_root: impl Into<PathBuf>,
65 package: impl Into<String>,
66 lib_name: impl Into<String>,
67 imports: impl IntoIterator<Item = impl Into<String>>,
68 ) -> Self {
69 Self {
70 project_root: project_root.into(),
71 package: package.into(),
72 lib_name: lib_name.into(),
73 imports: imports.into_iter().map(Into::into).collect(),
74 built_dylib_path: None,
75 built_capability: None,
76 worker_child: None,
77 startup_timeout: None,
78 request_timeout: None,
79 restart_policy: None,
80 metadata_check: None,
81 }
82 }
83
84 pub fn from_built_capability(
100 spec: &LeanBuiltCapability,
101 imports: impl IntoIterator<Item = impl Into<String>>,
102 ) -> Result<Self, LeanWorkerError> {
103 let artifact = WorkerCapabilityArtifact::from_built_capability(spec)?;
104 let project_root = infer_lake_project_root_from_dylib(&artifact.dylib_path)?;
105 Ok(Self {
106 project_root,
107 package: artifact.package,
108 lib_name: artifact.module,
109 imports: imports.into_iter().map(Into::into).collect(),
110 built_dylib_path: Some(artifact.dylib_path),
111 built_capability: Some(spec.clone()),
112 worker_child: None,
113 startup_timeout: None,
114 request_timeout: None,
115 restart_policy: None,
116 metadata_check: None,
117 })
118 }
119
120 #[must_use]
125 pub fn worker_executable(mut self, path: impl Into<PathBuf>) -> Self {
126 self.worker_child = Some(LeanWorkerChild::path(path));
127 self
128 }
129
130 #[must_use]
132 pub fn worker_child(mut self, child: LeanWorkerChild) -> Self {
133 self.worker_child = Some(child);
134 self
135 }
136
137 #[must_use]
139 pub fn startup_timeout(mut self, timeout: Duration) -> Self {
140 self.startup_timeout = Some(timeout);
141 self
142 }
143
144 #[must_use]
146 pub fn request_timeout(mut self, timeout: Duration) -> Self {
147 self.request_timeout = Some(timeout);
148 self
149 }
150
151 #[must_use]
153 pub fn long_running_requests(mut self) -> Self {
154 self.request_timeout = Some(LEAN_WORKER_REQUEST_TIMEOUT_LONG_RUNNING);
155 self
156 }
157
158 #[must_use]
160 pub fn restart_policy(mut self, policy: LeanWorkerRestartPolicy) -> Self {
161 self.restart_policy = Some(policy);
162 self
163 }
164
165 #[must_use]
171 pub fn validate_metadata(mut self, export: impl Into<String>, request: Value) -> Self {
172 self.metadata_check = Some(CapabilityMetadataCheck {
173 export: export.into(),
174 request,
175 expected: None,
176 });
177 self
178 }
179
180 #[must_use]
186 pub fn expect_metadata(
187 mut self,
188 export: impl Into<String>,
189 request: Value,
190 expected: LeanWorkerCapabilityMetadata,
191 ) -> Self {
192 self.metadata_check = Some(CapabilityMetadataCheck {
193 export: export.into(),
194 request,
195 expected: Some(expected),
196 });
197 self
198 }
199
200 #[must_use]
206 pub fn session_key(&self) -> LeanWorkerSessionKey {
207 let restart_policy_class = match &self.restart_policy {
208 Some(policy) if policy == &LeanWorkerRestartPolicy::default() => LeanWorkerRestartPolicyClass::Default,
209 Some(_policy) => LeanWorkerRestartPolicyClass::Custom,
210 None => LeanWorkerRestartPolicyClass::Default,
211 };
212 let mut key = LeanWorkerSessionKey::new(
213 self.project_root.clone(),
214 self.package.clone(),
215 self.lib_name.clone(),
216 self.imports.clone(),
217 )
218 .restart_policy_class(restart_policy_class);
219 if let Some(check) = &self.metadata_check {
220 key = key.metadata_expectation(check.export.clone(), check.request.clone(), check.expected.clone());
221 }
222 key
223 }
224
225 pub(crate) fn pool_request_timeout(&self) -> Duration {
226 self.request_timeout
227 .unwrap_or(crate::supervisor::LEAN_WORKER_REQUEST_TIMEOUT_DEFAULT)
228 }
229
230 #[must_use]
238 pub fn check(&self) -> LeanWorkerBootstrapReport {
239 let mut checks = self.bootstrap_static_checks();
240 if checks.iter().any(LeanWorkerBootstrapCheck::is_error) {
241 return LeanWorkerBootstrapReport::new(checks);
242 }
243
244 match self.clone().open_unchecked() {
245 Ok(capability) => {
246 drop(capability.terminate());
247 }
248 Err(err) => checks.push(check_from_open_error(&err)),
249 }
250 LeanWorkerBootstrapReport::new(checks)
251 }
252
253 fn bootstrap_static_checks(&self) -> Vec<LeanWorkerBootstrapCheck> {
254 let mut checks = Vec::new();
255 match self
256 .worker_child
257 .as_ref()
258 .map_or_else(resolve_default_worker_executable, LeanWorkerChild::resolve)
259 {
260 Ok(path) => {
261 if let Err(err) = validate_worker_child_path(&path) {
262 checks.push(check_from_open_error(&err));
263 }
264 }
265 Err(err) => checks.push(check_from_open_error(&err)),
266 }
267
268 if let Some(spec) = &self.built_capability
269 && spec.resolved_manifest_path().is_ok()
270 {
271 let report = LeanCapabilityPreflight::new(spec.clone()).check();
272 for check in report.errors() {
273 checks.push(LeanWorkerBootstrapCheck::error(
274 LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight { code: check.code() },
275 check.subject().to_owned(),
276 check.message().to_owned(),
277 check.repair_hint().to_owned(),
278 ));
279 }
280 }
281 checks
282 }
283
284 pub fn open(self) -> Result<LeanWorkerCapability, LeanWorkerError> {
292 let report = self.bootstrap_static_report();
293 if let Some(check) = report.first_error() {
294 return Err(LeanWorkerError::Bootstrap {
295 code: check.code(),
296 message: check.message().to_owned(),
297 });
298 }
299 self.open_unchecked()
300 }
301
302 fn bootstrap_static_report(&self) -> LeanWorkerBootstrapReport {
303 LeanWorkerBootstrapReport::new(self.bootstrap_static_checks())
304 }
305
306 fn open_unchecked(self) -> Result<LeanWorkerCapability, LeanWorkerError> {
307 let dylib_path = match self.built_dylib_path {
308 Some(path) => path,
309 None => lean_toolchain::build_lake_target_quiet(&self.project_root, &self.lib_name)
310 .map_err(|diagnostic| LeanWorkerError::CapabilityBuild { diagnostic })?,
311 };
312 let worker_executable = self
313 .worker_child
314 .map_or_else(resolve_default_worker_executable, |child| child.resolve())?;
315 validate_worker_child_path(&worker_executable)?;
316
317 let mut config = LeanWorkerConfig::new(worker_executable);
318 if let Some(timeout) = self.startup_timeout {
319 config = config.startup_timeout(timeout);
320 }
321 if let Some(timeout) = self.request_timeout {
322 config = config.request_timeout(timeout);
323 }
324 if let Some(policy) = self.restart_policy {
325 config = config.restart_policy(policy);
326 }
327
328 let mut worker = LeanWorker::spawn(&config)?;
329 worker.health()?;
330
331 let session_config = LeanWorkerSessionConfig::new(
332 self.project_root.clone(),
333 self.package.clone(),
334 self.lib_name.clone(),
335 self.imports.clone(),
336 );
337
338 let validated_metadata = {
339 let mut session = worker.open_session(&session_config, None, None)?;
340 match self.metadata_check {
341 Some(check) => {
342 let metadata = session.capability_metadata(&check.export, &check.request, None, None)?;
343 if let Some(expected) = check.expected
344 && metadata != expected
345 {
346 return Err(LeanWorkerError::CapabilityMetadataMismatch {
347 export: check.export,
348 expected: Box::new(expected),
349 actual: Box::new(metadata),
350 });
351 }
352 Some(metadata)
353 }
354 None => None,
355 }
356 };
357
358 Ok(LeanWorkerCapability {
359 worker,
360 session_config,
361 dylib_path,
362 validated_metadata,
363 })
364 }
365}
366
367#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
369#[non_exhaustive]
370pub enum LeanWorkerBootstrapDiagnosticCode {
371 WorkerChildUnresolved,
373 WorkerChildNotExecutable,
375 CapabilityPreflight { code: LeanLoaderDiagnosticCode },
377 WorkerHandshakeFailed,
379 CapabilityMetadataMismatch,
381 WorkerStartupFailed,
383}
384
385impl LeanWorkerBootstrapDiagnosticCode {
386 #[must_use]
388 pub const fn as_str(self) -> &'static str {
389 match self {
390 Self::WorkerChildUnresolved => "lean_rs.worker.bootstrap.child_unresolved",
391 Self::WorkerChildNotExecutable => "lean_rs.worker.bootstrap.child_not_executable",
392 Self::CapabilityPreflight { code } => code.as_str(),
393 Self::WorkerHandshakeFailed => "lean_rs.worker.bootstrap.handshake_failed",
394 Self::CapabilityMetadataMismatch => "lean_rs.worker.bootstrap.metadata_mismatch",
395 Self::WorkerStartupFailed => "lean_rs.worker.bootstrap.startup_failed",
396 }
397 }
398}
399
400impl std::fmt::Display for LeanWorkerBootstrapDiagnosticCode {
401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402 f.write_str(self.as_str())
403 }
404}
405
406#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
408#[non_exhaustive]
409pub enum LeanWorkerBootstrapSeverity {
410 Info,
412 Warning,
414 Error,
416}
417
418#[derive(Clone, Debug, Eq, PartialEq)]
420pub struct LeanWorkerBootstrapCheck {
421 code: LeanWorkerBootstrapDiagnosticCode,
422 severity: LeanWorkerBootstrapSeverity,
423 subject: String,
424 message: String,
425 repair_hint: String,
426}
427
428impl LeanWorkerBootstrapCheck {
429 fn error(
430 code: LeanWorkerBootstrapDiagnosticCode,
431 subject: impl Into<String>,
432 message: impl Into<String>,
433 repair_hint: impl Into<String>,
434 ) -> Self {
435 Self {
436 code,
437 severity: LeanWorkerBootstrapSeverity::Error,
438 subject: bound_bootstrap_text(subject.into()),
439 message: bound_bootstrap_text(message.into()),
440 repair_hint: bound_bootstrap_text(repair_hint.into()),
441 }
442 }
443
444 #[must_use]
446 pub fn code(&self) -> LeanWorkerBootstrapDiagnosticCode {
447 self.code
448 }
449
450 #[must_use]
452 pub fn severity(&self) -> LeanWorkerBootstrapSeverity {
453 self.severity
454 }
455
456 #[must_use]
458 pub fn subject(&self) -> &str {
459 &self.subject
460 }
461
462 #[must_use]
464 pub fn message(&self) -> &str {
465 &self.message
466 }
467
468 #[must_use]
470 pub fn repair_hint(&self) -> &str {
471 &self.repair_hint
472 }
473
474 fn is_error(&self) -> bool {
475 self.severity == LeanWorkerBootstrapSeverity::Error
476 }
477}
478
479#[derive(Clone, Debug, Eq, PartialEq)]
481pub struct LeanWorkerBootstrapReport {
482 checks: Vec<LeanWorkerBootstrapCheck>,
483}
484
485impl LeanWorkerBootstrapReport {
486 fn new(checks: Vec<LeanWorkerBootstrapCheck>) -> Self {
487 Self { checks }
488 }
489
490 #[must_use]
492 pub fn checks(&self) -> &[LeanWorkerBootstrapCheck] {
493 &self.checks
494 }
495
496 pub fn errors(&self) -> impl Iterator<Item = &LeanWorkerBootstrapCheck> {
498 self.checks
499 .iter()
500 .filter(|check| check.severity == LeanWorkerBootstrapSeverity::Error)
501 }
502
503 #[must_use]
505 pub fn is_ok(&self) -> bool {
506 self.first_error().is_none()
507 }
508
509 #[must_use]
511 pub fn first_error(&self) -> Option<&LeanWorkerBootstrapCheck> {
512 self.errors().next()
513 }
514}
515
516#[derive(Debug)]
522pub struct LeanWorkerCapability {
523 worker: LeanWorker,
524 session_config: LeanWorkerSessionConfig,
525 dylib_path: PathBuf,
526 validated_metadata: Option<LeanWorkerCapabilityMetadata>,
527}
528
529impl LeanWorkerCapability {
530 pub fn open_session(
542 &mut self,
543 cancellation: Option<&LeanWorkerCancellationToken>,
544 progress: Option<&dyn LeanWorkerProgressSink>,
545 ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
546 self.worker.open_session(&self.session_config, cancellation, progress)
547 }
548
549 #[must_use]
551 pub fn dylib_path(&self) -> &Path {
552 &self.dylib_path
553 }
554
555 #[must_use]
557 pub fn session_config(&self) -> &LeanWorkerSessionConfig {
558 &self.session_config
559 }
560
561 #[must_use]
563 pub fn validated_metadata(&self) -> Option<&LeanWorkerCapabilityMetadata> {
564 self.validated_metadata.as_ref()
565 }
566
567 #[must_use]
569 pub fn runtime_metadata(&self) -> LeanWorkerRuntimeMetadata {
570 self.worker.runtime_metadata()
571 }
572
573 #[must_use]
575 pub fn worker(&self) -> &LeanWorker {
576 &self.worker
577 }
578
579 #[must_use]
581 pub fn worker_mut(&mut self) -> &mut LeanWorker {
582 &mut self.worker
583 }
584
585 pub fn terminate(self) -> Result<crate::supervisor::LeanWorkerExit, LeanWorkerError> {
592 self.worker.terminate()
593 }
594}
595
596#[derive(Clone, Debug)]
597struct CapabilityMetadataCheck {
598 export: String,
599 request: Value,
600 expected: Option<LeanWorkerCapabilityMetadata>,
601}
602
603#[derive(Debug)]
604struct WorkerCapabilityArtifact {
605 dylib_path: PathBuf,
606 package: String,
607 module: String,
608}
609
610impl WorkerCapabilityArtifact {
611 fn from_built_capability(spec: &LeanBuiltCapability) -> Result<Self, LeanWorkerError> {
612 if let Ok(manifest_path) = spec.resolved_manifest_path() {
613 return Self::from_manifest(&manifest_path);
614 }
615
616 let dylib_path = spec.dylib_path().map_err(|err| LeanWorkerError::Setup {
617 message: err.to_string(),
618 })?;
619 let package = spec.package_name().ok_or_else(|| LeanWorkerError::Setup {
620 message: "LeanBuiltCapability is missing the Lake package name; call `.package(...)`".to_owned(),
621 })?;
622 let module = spec.module_name().ok_or_else(|| LeanWorkerError::Setup {
623 message: "LeanBuiltCapability is missing the root Lean module name; call `.module(...)`".to_owned(),
624 })?;
625 Ok(Self {
626 dylib_path,
627 package: package.to_owned(),
628 module: module.to_owned(),
629 })
630 }
631
632 fn from_manifest(manifest_path: &Path) -> Result<Self, LeanWorkerError> {
633 let bytes = std::fs::read(manifest_path).map_err(|err| LeanWorkerError::Bootstrap {
634 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
635 code: LeanLoaderDiagnosticCode::MissingManifest,
636 },
637 message: format!(
638 "could not read Lean capability manifest '{}': {err}",
639 manifest_path.display()
640 ),
641 })?;
642 let manifest: WorkerCapabilityManifest =
643 serde_json::from_slice(&bytes).map_err(|err| LeanWorkerError::Bootstrap {
644 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
645 code: LeanLoaderDiagnosticCode::MalformedManifest,
646 },
647 message: format!(
648 "Lean capability manifest '{}' is malformed: {err}",
649 manifest_path.display()
650 ),
651 })?;
652 if manifest.schema_version != u64::from(lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION) {
653 return Err(LeanWorkerError::Bootstrap {
654 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
655 code: LeanLoaderDiagnosticCode::UnsupportedManifestSchema,
656 },
657 message: format!(
658 "unsupported Lean capability manifest schema {}; supported schema is {}",
659 manifest.schema_version,
660 lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION
661 ),
662 });
663 }
664 Ok(Self {
665 dylib_path: manifest.primary_dylib,
666 package: manifest.package,
667 module: manifest.module,
668 })
669 }
670}
671
672#[derive(Deserialize)]
673struct WorkerCapabilityManifest {
674 schema_version: u64,
675 primary_dylib: PathBuf,
676 package: String,
677 module: String,
678}
679
680#[derive(Clone, Debug, Eq, PartialEq)]
687pub struct LeanWorkerChild {
688 executable_name: Option<String>,
689 explicit_path: Option<PathBuf>,
690 env_var: Option<String>,
691}
692
693impl LeanWorkerChild {
694 #[must_use]
697 pub fn sibling(executable_name: impl Into<String>) -> Self {
698 Self {
699 executable_name: Some(with_exe_suffix(executable_name.into())),
700 explicit_path: None,
701 env_var: None,
702 }
703 }
704
705 #[must_use]
707 pub fn path(path: impl Into<PathBuf>) -> Self {
708 Self {
709 executable_name: None,
710 explicit_path: Some(path.into()),
711 env_var: None,
712 }
713 }
714
715 #[must_use]
717 pub fn env_override(mut self, env_var: impl Into<String>) -> Self {
718 self.env_var = Some(env_var.into());
719 self
720 }
721
722 fn resolve(&self) -> Result<PathBuf, LeanWorkerError> {
723 let mut tried = Vec::new();
724 if let Some(env_var) = &self.env_var
725 && let Some(value) = env::var_os(env_var)
726 {
727 let path = PathBuf::from(value);
728 if path.is_file() {
729 return Ok(path);
730 }
731 tried.push(path);
732 return Err(LeanWorkerError::WorkerChildUnresolved { tried });
733 }
734 if let Some(path) = &self.explicit_path {
735 return Ok(path.clone());
736 }
737
738 let executable_name = self
739 .executable_name
740 .clone()
741 .unwrap_or_else(|| with_exe_suffix("lean-rs-worker-child".to_owned()));
742 tried.extend(candidate_sibling_worker_paths(&executable_name));
743 if executable_name == with_exe_suffix("lean-rs-worker-child".to_owned())
744 && let Some(path) = try_build_workspace_worker_child(&executable_name, &mut tried)
745 {
746 return Ok(path);
747 }
748 for path in dedup_paths(&tried) {
749 if path.is_file() {
750 return Ok(path);
751 }
752 }
753 Err(LeanWorkerError::WorkerChildUnresolved { tried })
754 }
755}
756
757impl Default for LeanWorkerChild {
758 fn default() -> Self {
759 Self::sibling("lean-rs-worker-child").env_override(WORKER_CHILD_ENV)
760 }
761}
762
763fn resolve_default_worker_executable() -> Result<PathBuf, LeanWorkerError> {
764 LeanWorkerChild::default().resolve()
765}
766
767fn validate_worker_child_path(path: &Path) -> Result<(), LeanWorkerError> {
768 if !path.is_file() {
769 return Err(LeanWorkerError::WorkerChildNotExecutable {
770 path: path.to_path_buf(),
771 reason: "path does not point to a file".to_owned(),
772 });
773 }
774 if !is_executable_file(path) {
775 return Err(LeanWorkerError::WorkerChildNotExecutable {
776 path: path.to_path_buf(),
777 reason: "file is not executable by this user".to_owned(),
778 });
779 }
780 Ok(())
781}
782
783#[cfg(unix)]
784fn is_executable_file(path: &Path) -> bool {
785 use std::os::unix::fs::PermissionsExt as _;
786
787 std::fs::metadata(path).is_ok_and(|metadata| metadata.permissions().mode() & 0o111 != 0)
788}
789
790#[cfg(not(unix))]
791fn is_executable_file(_path: &Path) -> bool {
792 true
793}
794
795fn check_from_open_error(err: &LeanWorkerError) -> LeanWorkerBootstrapCheck {
796 match err {
797 LeanWorkerError::WorkerChildUnresolved { tried } => LeanWorkerBootstrapCheck::error(
798 LeanWorkerBootstrapDiagnosticCode::WorkerChildUnresolved,
799 "worker child",
800 format!("could not resolve worker child; tried {}", format_paths(tried)),
801 "ship an app-owned worker child binary beside the app or configure LeanWorkerChild::env_override",
802 ),
803 LeanWorkerError::WorkerChildNotExecutable { path, reason } => LeanWorkerBootstrapCheck::error(
804 LeanWorkerBootstrapDiagnosticCode::WorkerChildNotExecutable,
805 path.display().to_string(),
806 reason.clone(),
807 "ship an app-owned worker child binary and ensure it is executable",
808 ),
809 LeanWorkerError::Bootstrap { code, message } => LeanWorkerBootstrapCheck::error(
810 *code,
811 code.as_str(),
812 message.clone(),
813 "fix the reported bootstrap input",
814 ),
815 LeanWorkerError::Handshake { message } => LeanWorkerBootstrapCheck::error(
816 LeanWorkerBootstrapDiagnosticCode::WorkerHandshakeFailed,
817 "worker handshake",
818 message.clone(),
819 "ensure the worker child calls lean_rs_worker::run_worker_child_stdio and matches this crate version",
820 ),
821 LeanWorkerError::Timeout {
822 operation: "startup", ..
823 } => LeanWorkerBootstrapCheck::error(
824 LeanWorkerBootstrapDiagnosticCode::WorkerHandshakeFailed,
825 "worker handshake",
826 err.to_string(),
827 "check that the worker child starts promptly and writes the lean-rs-worker handshake",
828 ),
829 LeanWorkerError::CapabilityMetadataMismatch { export, .. } => LeanWorkerBootstrapCheck::error(
830 LeanWorkerBootstrapDiagnosticCode::CapabilityMetadataMismatch,
831 export.clone(),
832 "capability metadata did not match the requested expectation",
833 "rebuild or select a capability whose metadata matches the caller expectation",
834 ),
835 other @ (LeanWorkerError::Spawn { .. }
836 | LeanWorkerError::CapabilityBuild { .. }
837 | LeanWorkerError::Setup { .. }
838 | LeanWorkerError::Protocol { .. }
839 | LeanWorkerError::Worker { .. }
840 | LeanWorkerError::ChildExited { .. }
841 | LeanWorkerError::ChildPanicOrAbort { .. }
842 | LeanWorkerError::Timeout { .. }
843 | LeanWorkerError::Cancelled { .. }
844 | LeanWorkerError::ProgressPanic { .. }
845 | LeanWorkerError::DataSinkPanic { .. }
846 | LeanWorkerError::DiagnosticSinkPanic { .. }
847 | LeanWorkerError::StreamExportFailed { .. }
848 | LeanWorkerError::StreamCallbackFailed { .. }
849 | LeanWorkerError::StreamRowMalformed { .. }
850 | LeanWorkerError::CapabilityMetadataMalformed { .. }
851 | LeanWorkerError::CapabilityDoctorMalformed { .. }
852 | LeanWorkerError::TypedCommandRequestEncode { .. }
853 | LeanWorkerError::TypedCommandResponseDecode { .. }
854 | LeanWorkerError::TypedCommandRowDecode { .. }
855 | LeanWorkerError::TypedCommandSummaryDecode { .. }
856 | LeanWorkerError::LeaseInvalidated { .. }
857 | LeanWorkerError::WorkerPoolExhausted { .. }
858 | LeanWorkerError::WorkerPoolMemoryBudgetExceeded { .. }
859 | LeanWorkerError::WorkerPoolQueueTimeout { .. }
860 | LeanWorkerError::UnsupportedRequest { .. }
861 | LeanWorkerError::Wait { .. }) => LeanWorkerBootstrapCheck::error(
862 LeanWorkerBootstrapDiagnosticCode::WorkerStartupFailed,
863 "worker bootstrap",
864 other.to_string(),
865 "run the bootstrap check in a deployment environment and rebuild the worker child or capability artifact",
866 ),
867 }
868}
869
870fn format_paths(paths: &[PathBuf]) -> String {
871 if paths.is_empty() {
872 return "<none>".to_owned();
873 }
874 paths
875 .iter()
876 .map(|path| path.display().to_string())
877 .collect::<Vec<_>>()
878 .join(", ")
879}
880
881fn bound_bootstrap_text(mut text: String) -> String {
882 const LIMIT: usize = 1_024;
883 if text.len() <= LIMIT {
884 return text;
885 }
886 while !text.is_char_boundary(LIMIT) {
887 text.pop();
888 }
889 text.truncate(LIMIT);
890 text.push_str("...");
891 text
892}
893
894fn candidate_sibling_worker_paths(executable_name: &str) -> Vec<PathBuf> {
895 let mut tried = Vec::new();
896 if let Ok(current_exe) = env::current_exe() {
897 if let Some(dir) = current_exe.parent() {
898 tried.push(dir.join(executable_name));
899 }
900 if let Some(profile_dir) = current_exe.parent().and_then(Path::parent) {
901 tried.push(profile_dir.join(executable_name));
902 }
903 }
904 tried
905}
906
907fn with_exe_suffix(mut executable_name: String) -> String {
908 if !env::consts::EXE_SUFFIX.is_empty() && !executable_name.ends_with(env::consts::EXE_SUFFIX) {
909 executable_name.push_str(env::consts::EXE_SUFFIX);
910 }
911 executable_name
912}
913
914fn infer_lake_project_root_from_dylib(dylib_path: &Path) -> Result<PathBuf, LeanWorkerError> {
915 let lib_dir = dylib_path.parent();
916 let build_dir = lib_dir.and_then(Path::parent);
917 let lake_dir = build_dir.and_then(Path::parent);
918 let project_root = lake_dir.and_then(Path::parent);
919 match (lib_dir, build_dir, lake_dir, project_root) {
920 (Some(lib), Some(build), Some(lake), Some(root))
921 if lib.file_name().is_some_and(|name| name == "lib")
922 && build.file_name().is_some_and(|name| name == "build")
923 && lake.file_name().is_some_and(|name| name == ".lake") =>
924 {
925 Ok(root.to_path_buf())
926 }
927 _ => Err(LeanWorkerError::Setup {
928 message: format!(
929 "built capability dylib '{}' is not under a standard .lake/build/lib directory",
930 dylib_path.display()
931 ),
932 }),
933 }
934}
935
936fn try_build_workspace_worker_child(executable_name: &str, tried: &mut Vec<PathBuf>) -> Option<PathBuf> {
937 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
938 let workspace = manifest_dir.parent()?.parent()?;
939 if !workspace
940 .join("crates")
941 .join("lean-rs-worker")
942 .join("Cargo.toml")
943 .is_file()
944 {
945 return None;
946 }
947
948 let debug = workspace.join("target").join("debug").join(executable_name);
949 let release = workspace.join("target").join("release").join(executable_name);
950 tried.push(debug.clone());
951 tried.push(release.clone());
952 if debug.is_file() {
953 return Some(debug);
954 }
955 if release.is_file() {
956 return Some(release);
957 }
958
959 let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
960 let status = Command::new(cargo)
961 .current_dir(workspace)
962 .args(["build", "-p", "lean-rs-worker", "--bin", "lean-rs-worker-child"])
963 .status()
964 .ok()?;
965 if !status.success() {
966 return None;
967 }
968 debug.is_file().then_some(debug)
969}
970
971fn dedup_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
972 let mut unique = Vec::new();
973 for path in paths {
974 if !unique.iter().any(|existing| existing == path) {
975 unique.push(path.clone());
976 }
977 }
978 unique
979}