@@ -7,6 +7,9 @@ use std::os::unix::net::UnixStream;
77#[ cfg( unix) ]
88use std:: os:: unix:: process:: CommandExt ;
99
10+ #[ cfg( windows) ]
11+ use std:: os:: windows:: io:: AsRawHandle ;
12+
1013#[ cfg( unix) ]
1114use crate :: audio:: pipe as audio_pipe;
1215use crate :: audio:: pipe:: SharedAnalysis ;
@@ -89,6 +92,9 @@ pub struct Player {
8992 has_parec : bool ,
9093 /// Real-time stream information from mpv
9194 pub stream_info : StreamInfo ,
95+ /// Windows Job Object handle — kills mpv automatically if AetherTune is closed via X button
96+ #[ cfg( windows) ]
97+ job_handle : Option < windows_sys:: Win32 :: Foundation :: HANDLE > ,
9298}
9399
94100impl Player {
@@ -107,6 +113,9 @@ impl Player {
107113 #[ cfg( windows) ]
108114 let has_parec = false ;
109115
116+ #[ cfg( windows) ]
117+ let job_handle = create_kill_on_close_job ( ) ;
118+
110119 Self {
111120 process : None ,
112121 #[ cfg( unix) ]
@@ -124,6 +133,8 @@ impl Player {
124133 request_counter : 0 ,
125134 has_parec,
126135 stream_info : StreamInfo :: new ( ) ,
136+ #[ cfg( windows) ]
137+ job_handle,
127138 }
128139 }
129140
@@ -154,6 +165,15 @@ impl Player {
154165
155166 match cmd. spawn ( ) {
156167 Ok ( c) => {
168+ // On Windows, assign mpv to our Job Object so it gets killed
169+ // automatically if the terminal window is closed via the X button.
170+ #[ cfg( windows) ]
171+ {
172+ if let Some ( job) = self . job_handle {
173+ assign_process_to_job ( job, c. as_raw_handle ( ) ) ;
174+ }
175+ }
176+
157177 self . process = Some ( c) ;
158178 self . media_title = None ;
159179 self . stream_info . reset ( ) ;
@@ -499,10 +519,61 @@ impl Player {
499519impl Drop for Player {
500520 fn drop ( & mut self ) {
501521 self . stop ( ) ;
522+
523+ #[ cfg( windows) ]
524+ {
525+ if let Some ( job) = self . job_handle . take ( ) {
526+ unsafe { windows_sys:: Win32 :: Foundation :: CloseHandle ( job) } ;
527+ }
528+ }
502529 }
503530}
504531
505532#[ cfg( unix) ]
506533fn shell_escape ( s : & str ) -> String {
507534 format ! ( "'{}'" , s. replace( '\'' , "'\\ ''" ) )
535+ }
536+
537+ /// Create a Windows Job Object configured to kill all assigned processes
538+ /// when the last handle to the job is closed (i.e. when AetherTune exits).
539+ #[ cfg( windows) ]
540+ fn create_kill_on_close_job ( ) -> Option < windows_sys:: Win32 :: Foundation :: HANDLE > {
541+ use windows_sys:: Win32 :: System :: JobObjects :: * ;
542+
543+ unsafe {
544+ let job = CreateJobObjectW ( std:: ptr:: null ( ) , std:: ptr:: null ( ) ) ;
545+ if job == 0 {
546+ return None ;
547+ }
548+
549+ let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std:: mem:: zeroed ( ) ;
550+ info. BasicLimitInformation . LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ;
551+
552+ let ok = SetInformationJobObject (
553+ job,
554+ JobObjectExtendedLimitInformation ,
555+ & info as * const _ as * const _ ,
556+ std:: mem:: size_of :: < JOBOBJECT_EXTENDED_LIMIT_INFORMATION > ( ) as u32 ,
557+ ) ;
558+
559+ if ok == 0 {
560+ windows_sys:: Win32 :: Foundation :: CloseHandle ( job) ;
561+ return None ;
562+ }
563+
564+ Some ( job)
565+ }
566+ }
567+
568+ /// Assign a spawned process to a Job Object so it inherits the kill-on-close behavior.
569+ #[ cfg( windows) ]
570+ fn assign_process_to_job (
571+ job : windows_sys:: Win32 :: Foundation :: HANDLE ,
572+ process : std:: os:: windows:: io:: RawHandle ,
573+ ) {
574+ use windows_sys:: Win32 :: System :: JobObjects :: AssignProcessToJobObject ;
575+
576+ unsafe {
577+ AssignProcessToJobObject ( job, process as isize ) ;
578+ }
508579}
0 commit comments