1use 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
440fn 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#[derive(Clone, Debug)]
456pub(crate) struct PactPluginInterceptor {
457 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#[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
722pub(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 #[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}