Skip to main content

pact_plugin_driver/
grpc_plugin.rs

1//! gRPC plugin wrapper and process management
2
3use std::process::Command;
4use std::process::Stdio;
5
6use anyhow::anyhow;
7use async_trait::async_trait;
8use log::max_level;
9use os_info::Type;
10use prost::Message;
11use std::path::PathBuf;
12use sysinfo::{Pid, System};
13use tonic::codegen::InterceptedService;
14use tonic::metadata::{Ascii, MetadataValue};
15use tonic::service::Interceptor;
16use tonic::transport::Channel;
17use tonic::{Request, Status};
18use tracing::{debug, warn};
19use uuid::Uuid;
20
21use crate::child_process::ChildPluginProcess;
22use crate::plugin_models::{
23  PactPlugin, PactPluginManifest, PactPluginRpc, PluginInitRequest, PluginInitResponse,
24  PluginInstance, PluginInterfaceVersion,
25};
26use crate::proto::pact_plugin_client::PactPluginClient as PactPluginClientV1;
27use crate::proto::*;
28use crate::proto_v2::{self, pact_plugin_client::PactPluginClient as PactPluginClientV2};
29
30pub(crate) enum PluginClient {
31  V1(PactPluginClientV1<InterceptedService<Channel, PactPluginInterceptor>>),
32  V2(PactPluginClientV2<InterceptedService<Channel, PactPluginInterceptor>>),
33}
34
35impl PluginClient {
36  pub(crate) fn convert_message<T, U>(message: T) -> Result<U, Status>
37  where
38    T: Message,
39    U: Message + Default,
40  {
41    U::decode(message.encode_to_vec().as_slice()).map_err(|err| {
42      Status::internal(format!(
43        "Failed to convert between plugin interface message versions: {}",
44        err
45      ))
46    })
47  }
48
49  async fn init_plugin(
50    &mut self,
51    request: PluginInitRequest,
52  ) -> Result<PluginInitResponse, Status> {
53    match self {
54      PluginClient::V1(client) => client
55        .init_plugin(Request::new(InitPluginRequest {
56          implementation: request.implementation,
57          version: request.version,
58        }))
59        .await
60        .map(|response| PluginInitResponse {
61          catalogue: response.into_inner().catalogue,
62          plugin_capabilities: vec![],
63        }),
64      PluginClient::V2(client) => client
65        .init_plugin(Request::new(proto_v2::InitPluginRequest {
66          implementation: request.implementation,
67          version: request.version,
68          host_capabilities: request.host_capabilities,
69          plugin_instance_id: request.plugin_instance_id,
70        }))
71        .await
72        .and_then(|response| match response.into_inner().response {
73          Some(proto_v2::init_plugin_response::Response::Success(success)) => {
74            Ok(PluginInitResponse {
75              catalogue: success
76                .catalogue
77                .into_iter()
78                .map(Self::convert_message)
79                .collect::<Result<Vec<CatalogueEntry>, Status>>()?,
80              plugin_capabilities: success.plugin_capabilities,
81            })
82          }
83          Some(proto_v2::init_plugin_response::Response::Failure(failure)) => {
84            let mut error = failure.error;
85            if !failure.missing_host_capabilities.is_empty() {
86              error.push_str(" (missing host capabilities: ");
87              error.push_str(failure.missing_host_capabilities.join(", ").as_str());
88              error.push(')');
89            }
90            Err(Status::failed_precondition(error))
91          }
92          None => Err(Status::internal(
93            "Plugin returned an invalid V2 InitPlugin response",
94          )),
95        }),
96    }
97  }
98
99  async fn compare_contents(
100    &mut self,
101    request: CompareContentsRequest,
102  ) -> Result<CompareContentsResponse, Status> {
103    match self {
104      PluginClient::V1(client) => client
105        .compare_contents(Request::new(request))
106        .await
107        .map(|response| response.into_inner()),
108      PluginClient::V2(client) => {
109        let mut v2_req = Self::convert_message::<_, proto_v2::CompareContentsRequest>(request)?;
110        if let Some(id) = crate::test_context::current_test_run_id() {
111          let ctx = v2_req.test_context.get_or_insert_with(prost_types::Struct::default);
112          ctx.fields.entry("testRunId".to_string()).or_insert_with(|| prost_types::Value {
113            kind: Some(prost_types::value::Kind::StringValue(id)),
114          });
115        }
116        client
117          .compare_contents(Request::new(v2_req))
118          .await
119          .and_then(|response| Self::convert_message(response.into_inner()))
120      }
121    }
122  }
123
124  async fn compare_contents_with_metadata(
125    &mut self,
126    request: CompareContentsRequest,
127    chain_id: &str,
128    deadline_ms: u64,
129  ) -> Result<CompareContentsResponse, Status> {
130    match self {
131      PluginClient::V1(client) => {
132        let mut req = Request::new(request);
133        insert_chain_metadata(&mut req, chain_id, deadline_ms)?;
134        client.compare_contents(req).await.map(|response| response.into_inner())
135      }
136      PluginClient::V2(client) => {
137        let mut v2_req = Self::convert_message::<_, proto_v2::CompareContentsRequest>(request)?;
138        if let Some(id) = crate::test_context::current_test_run_id() {
139          let ctx = v2_req.test_context.get_or_insert_with(prost_types::Struct::default);
140          ctx.fields.entry("testRunId".to_string()).or_insert_with(|| prost_types::Value {
141            kind: Some(prost_types::value::Kind::StringValue(id)),
142          });
143        }
144        let mut req = Request::new(v2_req);
145        insert_chain_metadata(&mut req, chain_id, deadline_ms)?;
146        client
147          .compare_contents(req)
148          .await
149          .and_then(|response| Self::convert_message(response.into_inner()))
150      }
151    }
152  }
153
154  async fn generate_content_with_metadata(
155    &mut self,
156    request: GenerateContentRequest,
157    chain_id: &str,
158    deadline_ms: u64,
159  ) -> Result<GenerateContentResponse, Status> {
160    match self {
161      PluginClient::V1(client) => {
162        let mut req = Request::new(request);
163        insert_chain_metadata(&mut req, chain_id, deadline_ms)?;
164        client.generate_content(req).await.map(|response| response.into_inner())
165      }
166      PluginClient::V2(client) => {
167        let mut req = Request::new(Self::convert_message::<_, proto_v2::GenerateContentRequest>(request)?);
168        insert_chain_metadata(&mut req, chain_id, deadline_ms)?;
169        client
170          .generate_content(req)
171          .await
172          .and_then(|response| Self::convert_message(response.into_inner()))
173      }
174    }
175  }
176
177  async fn configure_interaction(
178    &mut self,
179    request: ConfigureInteractionRequest,
180  ) -> Result<ConfigureInteractionResponse, Status> {
181    match self {
182      PluginClient::V1(client) => client
183        .configure_interaction(Request::new(request))
184        .await
185        .map(|response| response.into_inner()),
186      PluginClient::V2(client) => {
187        let mut v2_req =
188          Self::convert_message::<_, proto_v2::ConfigureInteractionRequest>(request)?;
189        if let Some(id) = crate::test_context::current_test_run_id() {
190          let ctx = v2_req.test_context.get_or_insert_with(prost_types::Struct::default);
191          ctx.fields.entry("testRunId".to_string()).or_insert_with(|| prost_types::Value {
192            kind: Some(prost_types::value::Kind::StringValue(id)),
193          });
194        }
195        client
196          .configure_interaction(Request::new(v2_req))
197          .await
198          .and_then(|response| Self::convert_message(response.into_inner()))
199      }
200    }
201  }
202
203  async fn generate_content(
204    &mut self,
205    request: GenerateContentRequest,
206  ) -> Result<GenerateContentResponse, Status> {
207    match self {
208      PluginClient::V1(client) => client
209        .generate_content(Request::new(request))
210        .await
211        .map(|response| response.into_inner()),
212      PluginClient::V2(client) => client
213        .generate_content(Request::new(Self::convert_message::<
214          _,
215          proto_v2::GenerateContentRequest,
216        >(request)?))
217        .await
218        .and_then(|response| Self::convert_message(response.into_inner())),
219    }
220  }
221
222  async fn start_mock_server(
223    &mut self,
224    request: StartMockServerRequest,
225  ) -> Result<StartMockServerResponse, Status> {
226    match self {
227      PluginClient::V1(client) => client
228        .start_mock_server(Request::new(request))
229        .await
230        .map(|response| response.into_inner()),
231      PluginClient::V2(client) => client
232        .start_mock_server(Request::new(Self::convert_message::<
233          _,
234          proto_v2::StartMockServerRequest,
235        >(request)?))
236        .await
237        .and_then(|response| Self::convert_message(response.into_inner())),
238    }
239  }
240
241  async fn shutdown_mock_server(
242    &mut self,
243    request: ShutdownMockServerRequest,
244  ) -> Result<ShutdownMockServerResponse, Status> {
245    match self {
246      PluginClient::V1(client) => client
247        .shutdown_mock_server(Request::new(request))
248        .await
249        .map(|response| response.into_inner()),
250      PluginClient::V2(client) => client
251        .shutdown_mock_server(Request::new(Self::convert_message::<
252          _,
253          proto_v2::MockServerRequest,
254        >(request)?))
255        .await
256        .and_then(|response| {
257          Self::convert_message::<_, ShutdownMockServerResponse>(response.into_inner())
258        }),
259    }
260  }
261
262  async fn match_field(
263    &mut self,
264    request: proto_v2::MatchFieldRequest,
265  ) -> Result<proto_v2::MatchFieldResponse, Status> {
266    match self {
267      PluginClient::V1(_) => Err(Status::unimplemented(
268        "Field-level matching rules require the V2 plugin interface, and this plugin uses V1"
269      )),
270      PluginClient::V2(client) => client
271        .match_field(Request::new(request))
272        .await
273        .map(|response| response.into_inner()),
274    }
275  }
276
277  async fn match_field_with_metadata(
278    &mut self,
279    request: proto_v2::MatchFieldRequest,
280    chain_id: &str,
281    deadline_ms: u64,
282  ) -> Result<proto_v2::MatchFieldResponse, Status> {
283    match self {
284      PluginClient::V1(_) => Err(Status::unimplemented(
285        "Field-level matching rules require the V2 plugin interface, and this plugin uses V1"
286      )),
287      PluginClient::V2(client) => {
288        let mut req = Request::new(request);
289        insert_chain_metadata(&mut req, chain_id, deadline_ms)?;
290        client.match_field(req).await.map(|response| response.into_inner())
291      }
292    }
293  }
294
295  async fn generate_field(
296    &mut self,
297    request: proto_v2::GenerateFieldRequest,
298  ) -> Result<proto_v2::GenerateFieldResponse, Status> {
299    match self {
300      PluginClient::V1(_) => Err(Status::unimplemented(
301        "Field-level generators require the V2 plugin interface, and this plugin uses V1"
302      )),
303      PluginClient::V2(client) => client
304        .generate_field(Request::new(request))
305        .await
306        .map(|response| response.into_inner()),
307    }
308  }
309
310  async fn generate_field_with_metadata(
311    &mut self,
312    request: proto_v2::GenerateFieldRequest,
313    chain_id: &str,
314    deadline_ms: u64,
315  ) -> Result<proto_v2::GenerateFieldResponse, Status> {
316    match self {
317      PluginClient::V1(_) => Err(Status::unimplemented(
318        "Field-level generators require the V2 plugin interface, and this plugin uses V1"
319      )),
320      PluginClient::V2(client) => {
321        let mut req = Request::new(request);
322        insert_chain_metadata(&mut req, chain_id, deadline_ms)?;
323        client.generate_field(req).await.map(|response| response.into_inner())
324      }
325    }
326  }
327
328  async fn start_mock_server_v2(
329    &mut self,
330    request: proto_v2::StartMockServerRequest,
331  ) -> Result<StartMockServerResponse, Status> {
332    match self {
333      PluginClient::V1(_) => Err(Status::unimplemented("V2 interface not supported on V1 plugin")),
334      PluginClient::V2(client) => client
335        .start_mock_server(Request::new(request))
336        .await
337        .and_then(|response| Self::convert_message(response.into_inner())),
338    }
339  }
340
341  async fn prepare_interaction_for_verification_v2(
342    &mut self,
343    request: proto_v2::VerificationPreparationRequest,
344  ) -> Result<VerificationPreparationResponse, Status> {
345    match self {
346      PluginClient::V1(_) => Err(Status::unimplemented("V2 interface not supported on V1 plugin")),
347      PluginClient::V2(client) => client
348        .prepare_interaction_for_verification(Request::new(request))
349        .await
350        .and_then(|response| Self::convert_message(response.into_inner())),
351    }
352  }
353
354  async fn verify_interaction_v2(
355    &mut self,
356    request: proto_v2::VerifyInteractionRequest,
357  ) -> Result<VerifyInteractionResponse, Status> {
358    match self {
359      PluginClient::V1(_) => Err(Status::unimplemented("V2 interface not supported on V1 plugin")),
360      PluginClient::V2(client) => client
361        .verify_interaction(Request::new(request))
362        .await
363        .and_then(|response| Self::convert_message(response.into_inner())),
364    }
365  }
366
367  async fn get_mock_server_results(
368    &mut self,
369    request: MockServerRequest,
370  ) -> Result<MockServerResults, Status> {
371    match self {
372      PluginClient::V1(client) => client
373        .get_mock_server_results(Request::new(request))
374        .await
375        .map(|response| response.into_inner()),
376      PluginClient::V2(client) => client
377        .get_mock_server_results(Request::new(Self::convert_message::<
378          _,
379          proto_v2::MockServerRequest,
380        >(request)?))
381        .await
382        .and_then(|response| Self::convert_message(response.into_inner())),
383    }
384  }
385
386  async fn prepare_interaction_for_verification(
387    &mut self,
388    request: VerificationPreparationRequest,
389  ) -> Result<VerificationPreparationResponse, Status> {
390    match self {
391      PluginClient::V1(client) => client
392        .prepare_interaction_for_verification(Request::new(request))
393        .await
394        .map(|response| response.into_inner()),
395      PluginClient::V2(client) => client
396        .prepare_interaction_for_verification(Request::new(Self::convert_message::<
397          _,
398          proto_v2::VerificationPreparationRequest,
399        >(request)?))
400        .await
401        .and_then(|response| Self::convert_message(response.into_inner())),
402    }
403  }
404
405  async fn verify_interaction(
406    &mut self,
407    request: VerifyInteractionRequest,
408  ) -> Result<VerifyInteractionResponse, Status> {
409    match self {
410      PluginClient::V1(client) => client
411        .verify_interaction(Request::new(request))
412        .await
413        .map(|response| response.into_inner()),
414      PluginClient::V2(client) => client
415        .verify_interaction(Request::new(Self::convert_message::<
416          _,
417          proto_v2::VerifyInteractionRequest,
418        >(request)?))
419        .await
420        .and_then(|response| Self::convert_message(response.into_inner())),
421    }
422  }
423
424  async fn update_catalogue(&mut self, request: Catalogue) -> Result<(), Status> {
425    match self {
426      PluginClient::V1(client) => client
427        .update_catalogue(Request::new(request))
428        .await
429        .map(|_| ()),
430      PluginClient::V2(client) => client
431        .update_catalogue(Request::new(
432          Self::convert_message::<_, proto_v2::Catalogue>(request)?,
433        ))
434        .await
435        .map(|_| ()),
436    }
437  }
438}
439
440/// Attach call-chain cycle detection and deadline metadata to an outbound request to a plugin,
441/// and bound the request's own gRPC timeout to the remaining deadline budget. See
442/// [`crate::call_chain`].
443fn insert_chain_metadata<T>(request: &mut Request<T>, chain_id: &str, deadline_ms: u64) -> Result<(), Status> {
444  let chain_value = MetadataValue::try_from(chain_id)
445    .map_err(|err| Status::internal(format!("Invalid call chain id '{}': {}", chain_id, err)))?;
446  let deadline_value = MetadataValue::try_from(deadline_ms.to_string())
447    .map_err(|err| Status::internal(format!("Invalid deadline value '{}': {}", deadline_ms, err)))?;
448  request.metadata_mut().insert(crate::call_chain::CALL_CHAIN_ID_METADATA_KEY, chain_value);
449  request.metadata_mut().insert(crate::call_chain::DEADLINE_METADATA_KEY, deadline_value);
450  request.set_timeout(crate::call_chain::remaining(deadline_ms));
451  Ok(())
452}
453
454/// Interceptor to inject the server key as an authorisation header
455#[derive(Clone, Debug)]
456pub(crate) struct PactPluginInterceptor {
457  /// Server key to inject
458  server_key: MetadataValue<Ascii>,
459}
460
461impl PactPluginInterceptor {
462  pub(crate) fn new(server_key: &str) -> anyhow::Result<Self> {
463    let token = MetadataValue::try_from(server_key)?;
464    Ok(PactPluginInterceptor { server_key: token })
465  }
466}
467
468impl Interceptor for PactPluginInterceptor {
469  fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
470    request
471      .metadata_mut()
472      .insert("authorization", self.server_key.clone());
473    Ok(request)
474  }
475}
476
477/// Wrapper around `PactPlugin` that provides gRPC connectivity
478#[derive(Debug, Clone)]
479pub struct GrpcPactPlugin {
480  pub plugin: PactPlugin,
481}
482
483impl GrpcPactPlugin {
484  pub fn new(plugin: PactPlugin) -> Self {
485    GrpcPactPlugin { plugin }
486  }
487
488  #[allow(deprecated)]
489  async fn connect_channel(&self) -> anyhow::Result<Channel> {
490    let port = self.plugin.child.port();
491    match Channel::from_shared(format!("http://[::1]:{}", port))?
492      .connect()
493      .await
494    {
495      Ok(channel) => Ok(channel),
496      Err(err) => {
497        debug!("IP6 connection failed, will try IP4 address - {err}");
498        Channel::from_shared(format!("http://127.0.0.1:{}", port))?
499          .connect()
500          .await
501          .map_err(|err| anyhow!(err))
502      }
503    }
504  }
505
506  #[allow(deprecated)]
507  async fn get_plugin_client(&self) -> anyhow::Result<PluginClient> {
508    let channel = self.connect_channel().await?;
509    let interceptor =
510      PactPluginInterceptor::new(self.plugin.child.plugin_info.server_key.as_str())?;
511    match self.plugin.interface_version {
512      PluginInterfaceVersion::V1 => Ok(PluginClient::V1(PactPluginClientV1::with_interceptor(
513        channel,
514        interceptor,
515      ))),
516      PluginInterfaceVersion::V2 => Ok(PluginClient::V2(PactPluginClientV2::with_interceptor(
517        channel,
518        interceptor,
519      ))),
520    }
521  }
522}
523
524#[async_trait]
525impl PactPluginRpc for GrpcPactPlugin {
526  async fn init_plugin(
527    &mut self,
528    request: PluginInitRequest,
529  ) -> anyhow::Result<PluginInitResponse> {
530    let mut client = self.get_plugin_client().await?;
531    client.init_plugin(request).await.map_err(anyhow::Error::from)
532  }
533}
534
535#[async_trait]
536impl PluginInstance for GrpcPactPlugin {
537  fn manifest(&self) -> &PactPluginManifest {
538    &self.plugin.manifest
539  }
540
541  fn instance_id(&self) -> &str {
542    &self.plugin.instance_id
543  }
544
545  fn has_capability(&self, capability: &str) -> bool {
546    self.plugin.has_capability(capability)
547  }
548
549  #[allow(deprecated)]
550  fn kill(&self) {
551    self.plugin.child.kill();
552  }
553
554  async fn compare_contents(
555    &self,
556    request: CompareContentsRequest,
557  ) -> anyhow::Result<CompareContentsResponse> {
558    let mut client = self.get_plugin_client().await?;
559    client.compare_contents(request).await.map_err(anyhow::Error::from)
560  }
561
562  async fn compare_contents_with_chain(
563    &self,
564    request: CompareContentsRequest,
565    chain_id: &str,
566    deadline_ms: u64,
567  ) -> anyhow::Result<CompareContentsResponse> {
568    let mut client = self.get_plugin_client().await?;
569    client
570      .compare_contents_with_metadata(request, chain_id, deadline_ms)
571      .await
572      .map_err(anyhow::Error::from)
573  }
574
575  async fn configure_interaction(
576    &self,
577    request: ConfigureInteractionRequest,
578  ) -> anyhow::Result<ConfigureInteractionResponse> {
579    let mut client = self.get_plugin_client().await?;
580    client.configure_interaction(request).await.map_err(anyhow::Error::from)
581  }
582
583  async fn generate_content(
584    &self,
585    request: GenerateContentRequest,
586  ) -> anyhow::Result<GenerateContentResponse> {
587    let mut client = self.get_plugin_client().await?;
588    client.generate_content(request).await.map_err(anyhow::Error::from)
589  }
590
591  async fn generate_content_with_chain(
592    &self,
593    request: GenerateContentRequest,
594    chain_id: &str,
595    deadline_ms: u64,
596  ) -> anyhow::Result<GenerateContentResponse> {
597    let mut client = self.get_plugin_client().await?;
598    client
599      .generate_content_with_metadata(request, chain_id, deadline_ms)
600      .await
601      .map_err(anyhow::Error::from)
602  }
603
604  async fn match_field(
605    &self,
606    request: proto_v2::MatchFieldRequest,
607  ) -> anyhow::Result<proto_v2::MatchFieldResponse> {
608    let mut client = self.get_plugin_client().await?;
609    client.match_field(request).await.map_err(anyhow::Error::from)
610  }
611
612  async fn match_field_with_chain(
613    &self,
614    request: proto_v2::MatchFieldRequest,
615    chain_id: &str,
616    deadline_ms: u64,
617  ) -> anyhow::Result<proto_v2::MatchFieldResponse> {
618    let mut client = self.get_plugin_client().await?;
619    client
620      .match_field_with_metadata(request, chain_id, deadline_ms)
621      .await
622      .map_err(anyhow::Error::from)
623  }
624
625  async fn generate_field(
626    &self,
627    request: proto_v2::GenerateFieldRequest,
628  ) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
629    let mut client = self.get_plugin_client().await?;
630    client.generate_field(request).await.map_err(anyhow::Error::from)
631  }
632
633  async fn generate_field_with_chain(
634    &self,
635    request: proto_v2::GenerateFieldRequest,
636    chain_id: &str,
637    deadline_ms: u64,
638  ) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
639    let mut client = self.get_plugin_client().await?;
640    client
641      .generate_field_with_metadata(request, chain_id, deadline_ms)
642      .await
643      .map_err(anyhow::Error::from)
644  }
645
646  async fn start_mock_server(
647    &self,
648    request: StartMockServerRequest,
649  ) -> anyhow::Result<StartMockServerResponse> {
650    let mut client = self.get_plugin_client().await?;
651    client.start_mock_server(request).await.map_err(anyhow::Error::from)
652  }
653
654  async fn start_mock_server_v2(
655    &self,
656    request: proto_v2::StartMockServerRequest,
657  ) -> anyhow::Result<StartMockServerResponse> {
658    let mut client = self.get_plugin_client().await?;
659    client.start_mock_server_v2(request).await.map_err(anyhow::Error::from)
660  }
661
662  async fn shutdown_mock_server(
663    &self,
664    request: ShutdownMockServerRequest,
665  ) -> anyhow::Result<ShutdownMockServerResponse> {
666    let mut client = self.get_plugin_client().await?;
667    client.shutdown_mock_server(request).await.map_err(anyhow::Error::from)
668  }
669
670  async fn get_mock_server_results(
671    &self,
672    request: MockServerRequest,
673  ) -> anyhow::Result<MockServerResults> {
674    let mut client = self.get_plugin_client().await?;
675    client.get_mock_server_results(request).await.map_err(anyhow::Error::from)
676  }
677
678  async fn prepare_interaction_for_verification(
679    &self,
680    request: VerificationPreparationRequest,
681  ) -> anyhow::Result<VerificationPreparationResponse> {
682    let mut client = self.get_plugin_client().await?;
683    client
684      .prepare_interaction_for_verification(request)
685      .await
686      .map_err(anyhow::Error::from)
687  }
688
689  async fn prepare_interaction_for_verification_v2(
690    &self,
691    request: proto_v2::VerificationPreparationRequest,
692  ) -> anyhow::Result<VerificationPreparationResponse> {
693    let mut client = self.get_plugin_client().await?;
694    client
695      .prepare_interaction_for_verification_v2(request)
696      .await
697      .map_err(anyhow::Error::from)
698  }
699
700  async fn verify_interaction(
701    &self,
702    request: VerifyInteractionRequest,
703  ) -> anyhow::Result<VerifyInteractionResponse> {
704    let mut client = self.get_plugin_client().await?;
705    client.verify_interaction(request).await.map_err(anyhow::Error::from)
706  }
707
708  async fn verify_interaction_v2(
709    &self,
710    request: proto_v2::VerifyInteractionRequest,
711  ) -> anyhow::Result<VerifyInteractionResponse> {
712    let mut client = self.get_plugin_client().await?;
713    client.verify_interaction_v2(request).await.map_err(anyhow::Error::from)
714  }
715
716  async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()> {
717    let mut client = self.get_plugin_client().await?;
718    client.update_catalogue(request).await.map_err(anyhow::Error::from)
719  }
720}
721
722/// Start a plugin process and return a PactPlugin with the child process attached
723pub(crate) async fn start_plugin_process(manifest: &PactPluginManifest) -> anyhow::Result<PactPlugin> {
724  debug!("Starting plugin with manifest {:?}", manifest);
725
726  let os_info = os_info::get();
727  debug!("Detected OS: {}", os_info);
728  let mut path = if let Some(entry_point) = manifest.entry_points.get(&os_info.to_string()) {
729    PathBuf::from(entry_point)
730  } else if os_info.os_type() == Type::Windows && manifest.entry_points.contains_key("windows") {
731    PathBuf::from(manifest.entry_points.get("windows").unwrap())
732  } else {
733    PathBuf::from(&manifest.entry_point)
734  };
735  if !path.is_absolute() || !path.exists() {
736    path = PathBuf::from(manifest.plugin_dir.clone()).join(path);
737  }
738  debug!("Starting plugin using {:?}", &path);
739
740  let host_port = match crate::plugin_host::ensure_plugin_host_running().await {
741    Ok(port) => Some(port),
742    Err(err) => {
743      warn!("Could not start PluginHost server, Log RPC forwarding will be unavailable: {}", err);
744      None
745    }
746  };
747
748  let log_level = max_level();
749  let mut child_command = Command::new(path.clone());
750  let mut child_command = child_command
751    .env("LOG_LEVEL", log_level.to_string())
752    .env("RUST_LOG", log_level.to_string())
753    .current_dir(manifest.plugin_dir.clone());
754
755  let instance_id = Uuid::new_v4().to_string();
756
757  child_command = child_command.env("PACT_PLUGIN_INSTANCE_ID", &instance_id);
758  if let Some(port) = host_port {
759    child_command = child_command.env("PACT_PLUGIN_HOST", format!("127.0.0.1:{}", port));
760  }
761
762  if let Some(args) = &manifest.args {
763    child_command = child_command.args(args);
764  }
765
766  let child = child_command
767    .stdout(Stdio::piped())
768    .stderr(Stdio::piped())
769    .spawn()
770    .map_err(|err| {
771      anyhow!(
772        "Was not able to start plugin process for '{}' - {}",
773        path.to_string_lossy(),
774        err
775      )
776    })?;
777  let child_pid = child.id();
778  debug!("Plugin {} started with PID {} (instance {})", manifest.name, child_pid, instance_id);
779  crate::plugin_manager::register_plugin_instance(&instance_id, &manifest.name);
780
781  match ChildPluginProcess::new(child, manifest, instance_id.clone()).await {
782    Ok(child) => {
783      let plugin = PactPlugin::new(manifest, child)?;
784      Ok(plugin)
785    }
786    Err(err) => {
787      crate::plugin_manager::deregister_plugin_instance(&instance_id);
788      let mut s = System::new();
789      s.refresh_processes();
790      if let Some(process) = s.process(Pid::from_u32(child_pid)) {
791        #[cfg(not(windows))]
792        process.kill();
793        // revert windows specific logic once https://github.com/GuillaumeGomez/sysinfo/pull/1341/files is merged/released
794        #[cfg(windows)]
795        let _ = Command::new("taskkill.exe")
796          .arg("/PID")
797          .arg(child_pid.to_string())
798          .arg("/F")
799          .arg("/T")
800          .output();
801      } else {
802        warn!("Child process with PID {} was not found", child_pid);
803      }
804      Err(err)
805    }
806  }
807}
808
809#[cfg(test)]
810pub(crate) mod tests {
811  use tonic::Status;
812
813  use crate::plugin_models::PluginInitResponse;
814  use crate::proto::*;
815
816  use super::PluginClient;
817
818  #[test]
819  fn converts_between_v1_and_v2_messages() {
820    use std::collections::HashMap;
821    use crate::plugin_models::PluginInitRequest;
822    use crate::proto_v2;
823
824    let request = PluginInitRequest {
825      implementation: "plugin-driver-rust".to_string(),
826      version: "1.0.0-beta.1".to_string(),
827      host_capabilities: vec!["interaction/request-response".to_string()],
828      plugin_instance_id: "test-instance-id".to_string(),
829    };
830
831    let converted_request = proto_v2::InitPluginRequest {
832      implementation: request.implementation,
833      version: request.version,
834      host_capabilities: request.host_capabilities,
835      plugin_instance_id: request.plugin_instance_id,
836    };
837    assert_eq!(converted_request.implementation, "plugin-driver-rust");
838    assert_eq!(converted_request.version, "1.0.0-beta.1");
839    assert_eq!(
840      converted_request.host_capabilities,
841      vec!["interaction/request-response"]
842    );
843
844    let response = proto_v2::InitPluginResponse {
845      response: Some(proto_v2::init_plugin_response::Response::Success(
846        proto_v2::InitPluginSuccess {
847          catalogue: vec![proto_v2::CatalogueEntry {
848            r#type: proto_v2::catalogue_entry::EntryType::ContentMatcher as i32,
849            key: "test".to_string(),
850            values: HashMap::new(),
851          }],
852          plugin_capabilities: vec!["plugin/verification".to_string()],
853        },
854      )),
855    };
856
857    let converted_response = match response.response.unwrap() {
858      proto_v2::init_plugin_response::Response::Success(success) => PluginInitResponse {
859        catalogue: success
860          .catalogue
861          .into_iter()
862          .map(PluginClient::convert_message)
863          .collect::<Result<Vec<CatalogueEntry>, Status>>()
864          .unwrap(),
865        plugin_capabilities: success.plugin_capabilities,
866      },
867      _ => unreachable!(),
868    };
869    assert_eq!(converted_response.catalogue.len(), 1);
870    assert_eq!(converted_response.catalogue[0].key, "test");
871    assert_eq!(
872      converted_response.catalogue[0].r#type,
873      catalogue_entry::EntryType::ContentMatcher as i32
874    );
875    assert_eq!(converted_response.plugin_capabilities, vec!["plugin/verification"]);
876  }
877}