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