@@ -56,9 +56,11 @@ struct RunArgs {
5656 /// KarpelesLab platform host (pullOne/submitFragment are anonymous — no key needed).
5757 #[ arg( long, env = "DECRYPTD_HOST" , default_value = "www.atonline.com" ) ]
5858 host : String ,
59- /// Working directory for the blob cache and scratch.
60- #[ arg( long, env = "DECRYPTD_WORKDIR" , default_value = "decryptd-data" ) ]
61- workdir : PathBuf ,
59+ /// Working directory for the blob cache, worker id, and scratch. Defaults to a
60+ /// per-user data dir for the GUI build (it may be launched from a non-writable
61+ /// CWD like System32) and to `./decryptd-data` for the console build.
62+ #[ arg( long, env = "DECRYPTD_WORKDIR" ) ]
63+ workdir : Option < PathBuf > ,
6264 /// Claim a single fragment then exit (default: loop forever).
6365 #[ arg( long) ]
6466 once : bool ,
@@ -81,6 +83,39 @@ struct RunArgs {
8183 cache_max_gb : u64 ,
8284}
8385
86+ impl RunArgs {
87+ /// The resolved working directory: the `--workdir`/`DECRYPTD_WORKDIR` value if
88+ /// given, else [`default_workdir`].
89+ fn workdir ( & self ) -> PathBuf {
90+ self . workdir . clone ( ) . unwrap_or_else ( default_workdir)
91+ }
92+ }
93+
94+ /// Default working directory. The GUI build can be started from any CWD (an
95+ /// Explorer double-click, a login item, a service), and a CWD-relative folder may
96+ /// not be creatable there — a silent failure that strands the tray on "Waiting".
97+ /// So the GUI build defaults to a stable per-user data dir; the console build,
98+ /// run from an operator-chosen directory, keeps `./decryptd-data`.
99+ fn default_workdir ( ) -> PathBuf {
100+ #[ cfg( feature = "gui" ) ]
101+ {
102+ #[ cfg( windows) ]
103+ if let Some ( base) = std:: env:: var_os ( "LOCALAPPDATA" ) {
104+ return PathBuf :: from ( base) . join ( "decryptd" ) ;
105+ }
106+ #[ cfg( not( windows) ) ]
107+ {
108+ if let Some ( base) = std:: env:: var_os ( "XDG_DATA_HOME" ) {
109+ return PathBuf :: from ( base) . join ( "decryptd" ) ;
110+ }
111+ if let Some ( home) = std:: env:: var_os ( "HOME" ) {
112+ return PathBuf :: from ( home) . join ( ".local/share/decryptd" ) ;
113+ }
114+ }
115+ }
116+ PathBuf :: from ( "decryptd-data" )
117+ }
118+
84119// ------------------------------------------------------------- pullOne response
85120/// `Decrypt/Job:pullOne` payload — a claimed fragment, its parent job's blobs, and
86121/// the single-use key that authenticates this fragment's result submission.
@@ -228,7 +263,7 @@ fn fetch_blob(args: &RunArgs, downloads: &Downloads, d: &DataRef) -> Result<Vec<
228263 return Ok ( bytes) ;
229264 }
230265
231- let cache = args. workdir . join ( "cache" ) ;
266+ let cache = args. workdir ( ) . join ( "cache" ) ;
232267 std:: fs:: create_dir_all ( & cache) ?;
233268 // `Hash` is the blob's content SHA-256, so it doubles as a cache key + checksum.
234269 let cache_path = ( !d. hash . is_empty ( ) ) . then ( || cache. join ( & d. hash ) ) ;
@@ -473,6 +508,10 @@ struct Status {
473508 active : Arc < AtomicUsize > ,
474509 /// Set by the tray's Pause item; the worker parks at its gates while true.
475510 paused : Arc < AtomicBool > ,
511+ /// Why the worker is idle when not running — "no open work", a pull error, or
512+ /// a fatal stop. Surfaced in the tray so a Windows user (whose console logs are
513+ /// swallowed by the GUI subsystem) can tell an idle worker from a broken one.
514+ note : Arc < Mutex < String > > ,
476515}
477516
478517impl Status {
@@ -513,6 +552,21 @@ impl Status {
513552 }
514553 }
515554
555+ /// Record why the worker is idle (shown in the tray). Set by the pipeline on
556+ /// pull errors / no-work and by the GUI on a fatal worker stop.
557+ fn set_note ( & self , note : impl Into < String > ) {
558+ * self . note . lock ( ) . unwrap ( ) = note. into ( ) ;
559+ }
560+
561+ /// The current idle note, or empty. Only read by the GUI tray.
562+ #[ cfg_attr(
563+ not( all( feature = "gui" , any( target_os = "linux" , target_os = "windows" ) ) ) ,
564+ allow( dead_code)
565+ ) ]
566+ fn note ( & self ) -> String {
567+ self . note . lock ( ) . unwrap ( ) . clone ( )
568+ }
569+
516570 /// Mark a GPU run as started; the returned guard marks it finished on drop
517571 /// (so a panic in `run_on_gpu` can't strand the counter above zero).
518572 fn run_guard ( & self ) -> RunGuard {
@@ -777,16 +831,19 @@ fn prefetch_loop(
777831 status. wait_while_paused ( ) ;
778832 match claim_and_fetch ( & args, & downloads, & worker_id, & ctx, & inflight) {
779833 Ok ( Some ( job) ) => {
834+ status. set_note ( "" ) ; // got work; clear any idle note
780835 if ready. send ( job) . is_err ( ) {
781836 return ; // pipeline shut down
782837 }
783838 }
784839 Ok ( None ) => {
785840 eprintln ! ( "[decryptd] no work; sleeping {}s" , args. idle_secs) ;
841+ status. set_note ( "no open work" ) ;
786842 thread:: sleep ( Duration :: from_secs ( args. idle_secs ) ) ;
787843 }
788844 Err ( e) => {
789845 eprintln ! ( "[decryptd] pull error: {e:#}" ) ;
846+ status. set_note ( format ! ( "pull error: {e}" ) ) ;
790847 thread:: sleep ( Duration :: from_secs ( args. idle_secs ) ) ;
791848 }
792849 }
@@ -952,11 +1009,13 @@ fn load_or_create_worker_id(workdir: &Path) -> Result<String> {
9521009}
9531010
9541011fn run_worker ( args : RunArgs , status : Status ) -> Result < ( ) > {
955- std:: fs:: create_dir_all ( & args. workdir ) ?;
1012+ let workdir = args. workdir ( ) ;
1013+ std:: fs:: create_dir_all ( & workdir)
1014+ . with_context ( || format ! ( "creating workdir {}" , workdir. display( ) ) ) ?;
9561015 let ctx = RestContext :: with_config ( Config :: new ( "https" . to_string ( ) , args. host . clone ( ) ) )
9571016 . with_debug ( std:: env:: var ( "DECRYPTD_DEBUG" ) . is_ok ( ) ) ;
9581017 let jobs = args. jobs . max ( 1 ) ;
959- let worker_id = load_or_create_worker_id ( & args . workdir ) ?;
1018+ let worker_id = load_or_create_worker_id ( & workdir) ?;
9601019
9611020 let count = cuda:: device_count ( ) . map_err ( |e| anyhow ! ( "enumerating GPUs: {e}" ) ) ?;
9621021 if count < 1 {
@@ -1073,6 +1132,17 @@ mod tests {
10731132 assert_eq ! ( bytes, b"hello" ) ;
10741133 }
10751134
1135+ #[ test]
1136+ fn workdir_override_beats_default ( ) {
1137+ // Explicit --workdir is always honored verbatim.
1138+ let a = RunArgs :: parse_from ( [ "decryptd" , "--workdir" , "/tmp/custom-wd" ] ) ;
1139+ assert_eq ! ( a. workdir( ) , PathBuf :: from( "/tmp/custom-wd" ) ) ;
1140+ // In the (non-gui) test build the default is the CWD-relative folder.
1141+ let b = RunArgs :: parse_from ( [ "decryptd" ] ) ;
1142+ assert_eq ! ( b. workdir( ) , default_workdir( ) ) ;
1143+ assert_eq ! ( default_workdir( ) , PathBuf :: from( "decryptd-data" ) ) ;
1144+ }
1145+
10761146 #[ test]
10771147 fn worker_id_persists_and_is_reused ( ) {
10781148 let dir = std:: env:: temp_dir ( ) . join ( format ! ( "decryptd-wid-{}" , std:: process:: id( ) ) ) ;
0 commit comments