Skip to main content

dprint_core/plugins/process/
communicator.rs

1use serde::de::DeserializeOwned;
2use std::cell::RefCell;
3use std::io::BufRead;
4use std::io::ErrorKind;
5use std::io::Read;
6use std::io::Write;
7use std::path::Path;
8use std::path::PathBuf;
9use std::process::Child;
10use std::process::ChildStderr;
11use std::process::Command;
12use std::process::Stdio;
13use std::rc::Rc;
14use std::sync::Arc;
15use std::time::Duration;
16use tokio::sync::oneshot;
17use tokio_util::sync::CancellationToken;
18
19use super::PLUGIN_SCHEMA_VERSION;
20use super::messages::CheckConfigUpdatesMessageBody;
21use super::messages::CheckConfigUpdatesResponseBody;
22use super::messages::FormatMessageBody;
23use super::messages::HostFormatMessageBody;
24use super::messages::MessageBody;
25use super::messages::ProcessPluginMessage;
26use super::messages::RegisterConfigMessageBody;
27use super::messages::ResponseBody;
28use crate::async_runtime::DropGuardAction;
29use crate::async_runtime::LocalBoxFuture;
30use crate::communication::AtomicFlag;
31use crate::communication::IdGenerator;
32use crate::communication::MessageReader;
33use crate::communication::MessageWriter;
34use crate::communication::RcIdStore;
35use crate::communication::SingleThreadMessageWriter;
36use crate::configuration::ConfigKeyMap;
37use crate::configuration::ConfigurationDiagnostic;
38use crate::configuration::GlobalConfiguration;
39use crate::plugins::ConfigChange;
40use crate::plugins::CriticalFormatError;
41use crate::plugins::FILE_MATCHING_INFO_ERROR_MESSAGE;
42use crate::plugins::FileMatchingInfo;
43use crate::plugins::FormatConfigId;
44use crate::plugins::FormatError;
45use crate::plugins::FormatRange;
46use crate::plugins::FormatResult;
47use crate::plugins::HostFormatRequest;
48use crate::plugins::NullCancellationToken;
49use crate::plugins::PluginInfo;
50use crate::plugins::error_to_string;
51
52type Result<T> = std::result::Result<T, FormatError>;
53
54#[cfg(unix)]
55use std::os::unix::process::CommandExt;
56
57type DprintCancellationToken = Arc<dyn super::super::CancellationToken>;
58
59/// The detailed reason a schema version negotiation step failed.
60#[derive(Debug, thiserror::Error)]
61enum SchemaVersionError {
62  #[error("Failed asking for schema version: {0}")]
63  Ask(std::io::Error),
64  #[error("Failed flushing schema version request: {0}")]
65  Flush(std::io::Error),
66  #[error("Could not read success response: {0}")]
67  ReadAcknowledgement(std::io::Error),
68  #[error("Plugin response was unexpected ({0}).")]
69  UnexpectedAcknowledgement(u32),
70  #[error("Could not read schema version: {0}")]
71  ReadVersion(std::io::Error),
72}
73
74/// Errors that can occur while communicating with a process plugin.
75#[derive(Debug, thiserror::Error)]
76enum CommunicatorError {
77  #[error("Error starting {executable} with args [{args}]. {error}")]
78  StartProcess { executable: String, args: String, error: std::io::Error },
79  #[error("Failed plugin schema verification. This may indicate you are using an old version of the dprint CLI or plugin and should upgrade")]
80  SchemaVerification(#[from] SchemaVersionError),
81  #[error(
82    "This plugin is too old to run in the dprint CLI and you will need to manually upgrade it (version was {actual}, but expected {expected}).\n\nUpgrade instructions: https://github.com/dprint/dprint/issues/731"
83  )]
84  PluginTooOld { actual: u32, expected: u32 },
85  #[error("Your dprint CLI is too old to run this plugin (version was {actual}, but expected {expected}). Try running: dprint upgrade")]
86  CliTooOld { actual: u32, expected: u32 },
87  #[error("Error waiting on message ({message_id}). {error}")]
88  WaitOnMessage {
89    message_id: u32,
90    error: tokio::sync::oneshot::error::RecvError,
91  },
92  #[error("Unexpected data channel for success response: {0}")]
93  UnexpectedDataForSuccess(u32),
94  #[error("Unexpected format channel for success response: {0}")]
95  UnexpectedFormatForSuccess(u32),
96  #[error("Unexpected success channel for data response: {0}")]
97  UnexpectedSuccessForData(u32),
98  #[error("Unexpected format channel for data response: {0}")]
99  UnexpectedFormatForData(u32),
100  #[error("Unexpected success channel for format response: {0}")]
101  UnexpectedSuccessForFormat(u32),
102  #[error("Unexpected data channel for format response: {0}")]
103  UnexpectedDataForFormat(u32),
104  #[error("Unknown message kind: {0}")]
105  UnknownMessageKind(u32),
106  #[error("Could not find host format callback for message id: {0}")]
107  HostFormatCallbackNotFound(u32),
108}
109
110impl From<CommunicatorError> for FormatError {
111  fn from(err: CommunicatorError) -> Self {
112    FormatError::new(err)
113  }
114}
115
116pub type HostFormatCallback = Rc<dyn Fn(HostFormatRequest) -> LocalBoxFuture<'static, FormatResult>>;
117
118pub struct ProcessPluginCommunicatorFormatRequest {
119  pub file_path: PathBuf,
120  pub file_bytes: Arc<Vec<u8>>,
121  pub range: FormatRange,
122  pub config_id: FormatConfigId,
123  pub override_config: ConfigKeyMap,
124  pub on_host_format: HostFormatCallback,
125  pub token: DprintCancellationToken,
126}
127
128enum MessageResponseChannel {
129  Acknowledgement(oneshot::Sender<Result<()>>),
130  Data(oneshot::Sender<Result<Vec<u8>>>),
131  Format(oneshot::Sender<Result<Option<Vec<u8>>>>),
132}
133
134struct Context {
135  stdin_writer: SingleThreadMessageWriter<ProcessPluginMessage>,
136  shutdown_flag: Arc<AtomicFlag>,
137  id_generator: IdGenerator,
138  messages: RcIdStore<MessageResponseChannel>,
139  format_request_tokens: RcIdStore<Arc<CancellationToken>>,
140  host_format_callbacks: RcIdStore<HostFormatCallback>,
141}
142
143/// Communicates with a process plugin.
144pub struct ProcessPluginCommunicator {
145  child: RefCell<Option<Child>>,
146  #[cfg(windows)]
147  job: Option<ProcessJob>,
148  context: Rc<Context>,
149}
150
151impl Drop for ProcessPluginCommunicator {
152  fn drop(&mut self) {
153    self.kill();
154  }
155}
156
157impl ProcessPluginCommunicator {
158  pub async fn new(executable_file_path: &Path, on_std_err: impl Fn(String) + Clone + Send + Sync + 'static) -> Result<Self> {
159    ProcessPluginCommunicator::new_internal(executable_file_path, false, on_std_err).await
160  }
161
162  /// Provides the `--init` CLI flag to tell the process plugin to do any initialization necessary
163  pub async fn new_with_init(executable_file_path: &Path, on_std_err: impl Fn(String) + Clone + Send + Sync + 'static) -> Result<Self> {
164    ProcessPluginCommunicator::new_internal(executable_file_path, true, on_std_err).await
165  }
166
167  async fn new_internal(executable_file_path: &Path, is_init: bool, on_std_err: impl Fn(String) + Clone + Send + Sync + 'static) -> Result<Self> {
168    let mut args = vec!["--parent-pid".to_string(), std::process::id().to_string()];
169    if is_init {
170      args.push("--init".to_string());
171    }
172
173    let shutdown_flag = Arc::new(AtomicFlag::default());
174    let mut command = Command::new(executable_file_path);
175    command.args(&args).stdin(Stdio::piped()).stderr(Stdio::piped()).stdout(Stdio::piped());
176    #[cfg(unix)]
177    command.process_group(0);
178    let mut child = command.spawn().map_err(|err| CommunicatorError::StartProcess {
179      executable: executable_file_path.display().to_string(),
180      args: args.join(" "),
181      error: err,
182    })?;
183    // assign the job right away so that it includes anything the plugin spawns
184    #[cfg(windows)]
185    let job = ProcessJob::for_child(&child);
186
187    // read and output stderr prefixed
188    let stderr = child.stderr.take().unwrap();
189    crate::async_runtime::spawn_blocking({
190      let shutdown_flag = shutdown_flag.clone();
191      let on_std_err = on_std_err.clone();
192      move || {
193        std_err_redirect(shutdown_flag, stderr, on_std_err);
194      }
195    });
196
197    // verify the schema version
198    let mut stdout_reader = MessageReader::new(child.stdout.take().unwrap());
199    let mut stdin_writer = MessageWriter::new(child.stdin.take().unwrap());
200
201    let (mut stdout_reader, stdin_writer, schema_version) = crate::async_runtime::spawn_blocking(move || {
202      let schema_version = get_plugin_schema_version(&mut stdout_reader, &mut stdin_writer).map_err(CommunicatorError::SchemaVerification)?;
203      Ok::<_, FormatError>((stdout_reader, stdin_writer, schema_version))
204    })
205    .await??;
206
207    if schema_version != PLUGIN_SCHEMA_VERSION {
208      // kill the child to prevent it from ouputting to stderr
209      let _ = child.kill();
210      let err = if schema_version < PLUGIN_SCHEMA_VERSION {
211        CommunicatorError::PluginTooOld {
212          actual: schema_version,
213          expected: PLUGIN_SCHEMA_VERSION,
214        }
215      } else {
216        CommunicatorError::CliTooOld {
217          actual: schema_version,
218          expected: PLUGIN_SCHEMA_VERSION,
219        }
220      };
221      return Err(err.into());
222    }
223
224    let stdin_writer = SingleThreadMessageWriter::for_stdin(stdin_writer);
225    let context = Rc::new(Context {
226      id_generator: Default::default(),
227      shutdown_flag,
228      stdin_writer,
229      messages: Default::default(),
230      format_request_tokens: Default::default(),
231      host_format_callbacks: Default::default(),
232    });
233
234    // read from stdout
235    let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
236    crate::async_runtime::spawn_blocking({
237      let shutdown_flag = context.shutdown_flag.clone();
238      let on_std_err = on_std_err.clone();
239      move || {
240        loop {
241          match ProcessPluginMessage::read(&mut stdout_reader) {
242            Ok(message) => {
243              if tx.send(message).is_err() {
244                break; // closed
245              }
246            }
247            Err(err) if err.kind() == ErrorKind::BrokenPipe => {
248              break;
249            }
250            Err(err) => {
251              if !shutdown_flag.is_raised() {
252                on_std_err(format!("Error reading stdout message: {:#}", err));
253              }
254              break;
255            }
256          }
257        }
258      }
259    });
260    crate::async_runtime::spawn({
261      let context = context.clone();
262      async move {
263        while let Some(message) = rx.recv().await {
264          if let Err(err) = handle_stdout_message(message, &context) {
265            if !context.shutdown_flag.is_raised() {
266              on_std_err(format!("Error reading stdout message: {:#}", err));
267            }
268            break;
269          }
270        }
271        // clear out all the messages
272        context.messages.take_all();
273      }
274    });
275
276    Ok(Self {
277      #[cfg(windows)]
278      job,
279      child: RefCell::new(Some(child)),
280      context,
281    })
282  }
283
284  /// Perform a graceful shutdown.
285  pub async fn shutdown(&self) {
286    if self.context.shutdown_flag.raise() {
287      // attempt to exit nicely
288      tokio::select! {
289        // we wait for acknowledgement in order to give the process
290        // plugin a chance to clean up (ex. in case it has spawned
291        // any processes it needs to kill or something like that)
292        _ = self.send_with_acknowledgement(MessageBody::Close) => {}
293        _ = tokio::time::sleep(Duration::from_millis(250)) => {
294          self.kill();
295        }
296      }
297    } else {
298      self.kill();
299    }
300  }
301
302  pub fn kill(&self) {
303    self.context.shutdown_flag.raise();
304    if let Some(mut child) = self.child.borrow_mut().take() {
305      #[cfg(unix)]
306      unsafe {
307        libc::kill(-(child.id() as i32), libc::SIGKILL);
308      }
309      #[cfg(windows)]
310      if let Some(job) = &self.job {
311        job.terminate();
312      }
313      let _ignore = child.kill();
314    }
315  }
316
317  pub async fn register_config(&self, config_id: FormatConfigId, global_config: &GlobalConfiguration, plugin_config: &ConfigKeyMap) -> Result<()> {
318    let global_config = serde_json::to_vec(global_config)?;
319    let plugin_config = serde_json::to_vec(plugin_config)?;
320    self
321      .send_with_acknowledgement(MessageBody::RegisterConfig(RegisterConfigMessageBody {
322        config_id,
323        global_config,
324        plugin_config,
325      }))
326      .await?;
327    Ok(())
328  }
329
330  pub async fn release_config(&self, config_id: FormatConfigId) -> Result<()> {
331    self.send_with_acknowledgement(MessageBody::ReleaseConfig(config_id)).await?;
332    Ok(())
333  }
334
335  pub async fn ask_is_alive(&self) -> bool {
336    self.send_with_acknowledgement(MessageBody::IsAlive).await.is_ok()
337  }
338
339  pub async fn plugin_info(&self) -> Result<PluginInfo> {
340    self.send_receiving_data(MessageBody::GetPluginInfo).await
341  }
342
343  pub async fn license_text(&self) -> Result<String> {
344    self.send_receiving_string(MessageBody::GetLicenseText).await
345  }
346
347  pub async fn resolved_config(&self, config_id: FormatConfigId) -> Result<String> {
348    self.send_receiving_string(MessageBody::GetResolvedConfig(config_id)).await
349  }
350
351  pub async fn file_matching_info(&self, config_id: FormatConfigId) -> Result<FileMatchingInfo> {
352    let data = self.send_receiving_bytes(MessageBody::GetFileMatchingInfo(config_id)).await??;
353    serde_json::from_slice(&data).map_err(|err| FormatError::new(format!("{FILE_MATCHING_INFO_ERROR_MESSAGE}\n\n{err:#}")))
354  }
355
356  pub async fn config_diagnostics(&self, config_id: FormatConfigId) -> Result<Vec<ConfigurationDiagnostic>> {
357    self.send_receiving_data(MessageBody::GetConfigDiagnostics(config_id)).await
358  }
359
360  pub async fn check_config_updates(&self, message: &CheckConfigUpdatesMessageBody) -> Result<Vec<ConfigChange>> {
361    let bytes = serde_json::to_vec(&message)?;
362    let response: CheckConfigUpdatesResponseBody = self.send_receiving_data(MessageBody::CheckConfigUpdates(bytes)).await?;
363    Ok(response.changes)
364  }
365
366  pub async fn format_text(&self, request: ProcessPluginCommunicatorFormatRequest) -> FormatResult {
367    let (tx, rx) = oneshot::channel::<Result<Option<Vec<u8>>>>();
368
369    let message_id = self.context.id_generator.next();
370    let store_guard = self.context.host_format_callbacks.store_with_guard(message_id, request.on_host_format);
371    let maybe_result = self
372      .send_message_with_id(
373        message_id,
374        MessageBody::FormatShared(FormatMessageBody {
375          file_path: request.file_path,
376          file_bytes: request.file_bytes,
377          range: request.range,
378          config_id: request.config_id,
379          override_config: serde_json::to_vec(&request.override_config).unwrap(),
380        }),
381        MessageResponseChannel::Format(tx),
382        rx,
383        request.token.clone(),
384      )
385      .await;
386
387    drop(store_guard); // explicit for clarity
388
389    if request.token.is_cancelled() {
390      Ok(None)
391    } else {
392      match maybe_result {
393        Ok(result) => result,
394        Err(err) => Err(CriticalFormatError(err).into()),
395      }
396    }
397  }
398
399  /// Checks if the process is functioning.
400  pub async fn is_process_alive(&self) -> bool {
401    if self.context.shutdown_flag.is_raised() {
402      false
403    } else {
404      self.ask_is_alive().await
405    }
406  }
407
408  async fn send_with_acknowledgement(&self, body: MessageBody) -> Result<()> {
409    let (tx, rx) = oneshot::channel::<Result<()>>();
410    self
411      .send_message(body, MessageResponseChannel::Acknowledgement(tx), rx, Arc::new(NullCancellationToken))
412      .await?
413  }
414
415  async fn send_receiving_string(&self, body: MessageBody) -> Result<String> {
416    let data = self.send_receiving_bytes(body).await??;
417    Ok(String::from_utf8(data)?)
418  }
419
420  async fn send_receiving_data<T: DeserializeOwned>(&self, body: MessageBody) -> Result<T> {
421    let data = self.send_receiving_bytes(body).await??;
422    Ok(serde_json::from_slice(&data)?)
423  }
424
425  async fn send_receiving_bytes(&self, body: MessageBody) -> Result<Result<Vec<u8>>> {
426    let (tx, rx) = oneshot::channel::<Result<Vec<u8>>>();
427    self
428      .send_message(body, MessageResponseChannel::Data(tx), rx, Arc::new(NullCancellationToken))
429      .await
430  }
431
432  async fn send_message<T: Default>(
433    &self,
434    body: MessageBody,
435    response_channel: MessageResponseChannel,
436    receiver: oneshot::Receiver<Result<T>>,
437    token: Arc<dyn super::super::CancellationToken>,
438  ) -> Result<Result<T>> {
439    let message_id = self.context.id_generator.next();
440    self.send_message_with_id(message_id, body, response_channel, receiver, token).await
441  }
442
443  async fn send_message_with_id<T: Default>(
444    &self,
445    message_id: u32,
446    body: MessageBody,
447    response_channel: MessageResponseChannel,
448    receiver: oneshot::Receiver<Result<T>>,
449    token: Arc<dyn super::super::CancellationToken>,
450  ) -> Result<Result<T>> {
451    let mut drop_guard = DropGuardAction::new(|| {
452      // clear up memory
453      self.context.messages.take(message_id);
454      // send cancellation to the client
455      let _ = self.context.stdin_writer.send(ProcessPluginMessage {
456        id: self.context.id_generator.next(),
457        body: MessageBody::CancelFormat(message_id),
458      });
459    });
460
461    self.context.messages.store(message_id, response_channel);
462    self.context.stdin_writer.send(ProcessPluginMessage { id: message_id, body })?;
463    tokio::select! {
464      _ = token.wait_cancellation() => {
465        drop(drop_guard); // explicit
466        Ok(Ok(Default::default()))
467      }
468      response = receiver => {
469        drop_guard.forget(); // we completed, so don't run the drop guard
470        match response {
471          Ok(data) => Ok(data),
472          Err(err) => Err(CommunicatorError::WaitOnMessage { message_id, error: err }.into()),
473        }
474      }
475    }
476  }
477}
478
479fn get_plugin_schema_version<TRead: Read + Unpin, TWrite: Write + Unpin>(
480  reader: &mut MessageReader<TRead>,
481  writer: &mut MessageWriter<TWrite>,
482) -> std::result::Result<u32, SchemaVersionError> {
483  // since this is the setup, identify exactly where it failed
484  writer.send_u32(0).map_err(SchemaVersionError::Ask)?; // ask for schema version
485  writer.flush().map_err(SchemaVersionError::Flush)?;
486  let acknowledgement_response = reader.read_u32().map_err(SchemaVersionError::ReadAcknowledgement)?;
487  if acknowledgement_response != 0 {
488    return Err(SchemaVersionError::UnexpectedAcknowledgement(acknowledgement_response));
489  }
490  reader.read_u32().map_err(SchemaVersionError::ReadVersion)
491}
492
493fn std_err_redirect(shutdown_flag: Arc<AtomicFlag>, stderr: ChildStderr, on_std_err: impl Fn(String) + Send + Sync + 'static) {
494  let reader = std::io::BufReader::new(stderr);
495  for line in reader.lines() {
496    match line {
497      Ok(line) => on_std_err(line),
498      Err(err) => {
499        if shutdown_flag.is_raised() || err.kind() == ErrorKind::BrokenPipe {
500          return;
501        } else {
502          on_std_err(format!("Error reading line from process plugin stderr. {:#}", err));
503        }
504      }
505    }
506  }
507}
508
509fn handle_stdout_message(message: ProcessPluginMessage, context: &Rc<Context>) -> Result<()> {
510  match message.body {
511    MessageBody::Success(message_id) => match context.messages.take(message_id) {
512      Some(MessageResponseChannel::Acknowledgement(channel)) => {
513        let _ignore = channel.send(Ok(()));
514      }
515      Some(MessageResponseChannel::Data(channel)) => {
516        let _ignore = channel.send(Err(CommunicatorError::UnexpectedDataForSuccess(message_id).into()));
517      }
518      Some(MessageResponseChannel::Format(channel)) => {
519        let _ignore = channel.send(Err(CommunicatorError::UnexpectedFormatForSuccess(message_id).into()));
520      }
521      None => {}
522    },
523    MessageBody::DataResponse(response) => match context.messages.take(response.message_id) {
524      Some(MessageResponseChannel::Acknowledgement(channel)) => {
525        let _ignore = channel.send(Err(CommunicatorError::UnexpectedSuccessForData(response.message_id).into()));
526      }
527      Some(MessageResponseChannel::Data(channel)) => {
528        let _ignore = channel.send(Ok(response.data));
529      }
530      Some(MessageResponseChannel::Format(channel)) => {
531        let _ignore = channel.send(Err(CommunicatorError::UnexpectedFormatForData(response.message_id).into()));
532      }
533      None => {}
534    },
535    MessageBody::Error(response) => {
536      let err: FormatError = String::from_utf8_lossy(&response.data).into_owned().into();
537      match context.messages.take(response.message_id) {
538        Some(MessageResponseChannel::Acknowledgement(channel)) => {
539          let _ignore = channel.send(Err(err));
540        }
541        Some(MessageResponseChannel::Data(channel)) => {
542          let _ignore = channel.send(Err(err));
543        }
544        Some(MessageResponseChannel::Format(channel)) => {
545          let _ignore = channel.send(Err(err));
546        }
547        None => {}
548      }
549    }
550    MessageBody::FormatResponse(response) => match context.messages.take(response.message_id) {
551      Some(MessageResponseChannel::Acknowledgement(channel)) => {
552        let _ignore = channel.send(Err(CommunicatorError::UnexpectedSuccessForFormat(response.message_id).into()));
553      }
554      Some(MessageResponseChannel::Data(channel)) => {
555        let _ignore = channel.send(Err(CommunicatorError::UnexpectedDataForFormat(response.message_id).into()));
556      }
557      Some(MessageResponseChannel::Format(channel)) => {
558        let _ignore = channel.send(Ok(response.data));
559      }
560      None => {}
561    },
562    MessageBody::CancelFormat(message_id) => {
563      if let Some(token) = context.format_request_tokens.take(message_id) {
564        token.cancel();
565      }
566      context.host_format_callbacks.take(message_id);
567      // do not clear from context.messages here because the cancellation will do that
568    }
569    MessageBody::HostFormat(body) => {
570      // spawn a task to do the host formatting, then send a message back to the
571      // plugin with the result
572      let context = context.clone();
573      crate::async_runtime::spawn(async move {
574        let result = host_format(context.clone(), message.id, body).await;
575
576        // ignore failure, as this means that the process shut down
577        // at which point handling would have occurred elsewhere
578        let _ignore = context.stdin_writer.send(ProcessPluginMessage {
579          id: context.id_generator.next(),
580          body: match result {
581            Ok(result) => MessageBody::FormatResponse(ResponseBody {
582              message_id: message.id,
583              data: result,
584            }),
585            Err(err) => MessageBody::Error(ResponseBody {
586              message_id: message.id,
587              data: error_to_string(&err).into_bytes(),
588            }),
589          },
590        });
591      });
592    }
593    MessageBody::IsAlive => {
594      // the CLI is not documented as supporting this, but we might as well respond
595      let _ = context.stdin_writer.send(ProcessPluginMessage {
596        id: context.id_generator.next(),
597        body: MessageBody::Success(message.id),
598      });
599    }
600    MessageBody::Format(_)
601    | MessageBody::FormatShared(_)
602    | MessageBody::Close
603    | MessageBody::GetPluginInfo
604    | MessageBody::GetLicenseText
605    | MessageBody::RegisterConfig(_)
606    | MessageBody::ReleaseConfig(_)
607    | MessageBody::GetConfigDiagnostics(_)
608    | MessageBody::GetFileMatchingInfo(_)
609    | MessageBody::GetResolvedConfig(_)
610    | MessageBody::CheckConfigUpdates(_) => {
611      let _ = context.stdin_writer.send(ProcessPluginMessage {
612        id: context.id_generator.next(),
613        body: MessageBody::Error(ResponseBody {
614          message_id: message.id,
615          data: "Unsupported plugin to CLI message.".as_bytes().to_vec(),
616        }),
617      });
618    }
619    // If encountered, process plugin should exit and
620    // the CLI should kill the process plugin.
621    MessageBody::Unknown(message_kind) => {
622      return Err(CommunicatorError::UnknownMessageKind(message_kind).into());
623    }
624  }
625
626  Ok(())
627}
628
629async fn host_format(context: Rc<Context>, message_id: u32, body: HostFormatMessageBody) -> FormatResult {
630  let Some(callback) = context.host_format_callbacks.get_cloned(body.original_message_id) else {
631    return FormatResult::Err(CommunicatorError::HostFormatCallbackNotFound(body.original_message_id).into());
632  };
633
634  let token = Arc::new(CancellationToken::new());
635  let store_guard = context.format_request_tokens.store_with_guard(message_id, token.clone());
636  let result = callback(HostFormatRequest {
637    file_path: body.file_path,
638    file_bytes: body.file_text,
639    range: body.range,
640    override_config: serde_json::from_slice(&body.override_config).unwrap(),
641    token,
642  })
643  .await;
644  drop(store_guard); // explicit for clarity
645  result
646}
647
648/// A Windows job object the process plugin is assigned to so that any
649/// subprocesses it spawns get killed along with it.
650#[cfg(windows)]
651struct ProcessJob(winapi::um::winnt::HANDLE);
652
653#[cfg(windows)]
654impl ProcessJob {
655  /// Creates a job and assigns the child to it. Processes the child spawns
656  /// from this point on are automatically part of the job.
657  fn for_child(child: &Child) -> Option<Self> {
658    use std::os::windows::io::AsRawHandle;
659    use winapi::um::jobapi2::AssignProcessToJobObject;
660    use winapi::um::jobapi2::CreateJobObjectW;
661
662    unsafe {
663      let handle = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());
664      if handle.is_null() {
665        return None;
666      }
667      let job = Self(handle);
668      if AssignProcessToJobObject(job.0, child.as_raw_handle() as _) == 0 {
669        return None;
670      }
671      Some(job)
672    }
673  }
674
675  /// Kills every process in the job.
676  fn terminate(&self) {
677    unsafe {
678      winapi::um::jobapi2::TerminateJobObject(self.0, 1);
679    }
680  }
681}
682
683#[cfg(windows)]
684impl Drop for ProcessJob {
685  fn drop(&mut self) {
686    unsafe {
687      winapi::um::handleapi::CloseHandle(self.0);
688    }
689  }
690}