ledger_sim/drivers/
docker.rs1use std::{
5 collections::HashMap,
6 net::{IpAddr, Ipv4Addr, SocketAddr},
7 path::PathBuf,
8 time::Duration,
9};
10
11use async_trait::async_trait;
12use bollard::{
13 container::{
14 Config, CreateContainerOptions, LogsOptions, RemoveContainerOptions, StartContainerOptions,
15 StopContainerOptions, UploadToContainerOptions,
16 },
17 service::{ContainerStateStatusEnum, HostConfig, PortBinding},
18 Docker,
19};
20use bytes::{BufMut, BytesMut};
21use futures::StreamExt;
22use tokio::sync::oneshot::{channel, Sender};
23use tracing::debug;
24
25use super::Driver;
26use crate::{Handle, Options};
27
28pub struct DockerDriver {
30 d: Docker,
31}
32
33#[derive(Debug)]
35pub struct DockerHandle {
36 name: String,
37 addr: SocketAddr,
38 exit_tx: Sender<()>,
39}
40
41impl DockerDriver {
42 pub fn new() -> Result<Self, anyhow::Error> {
44 let d = Docker::connect_with_local_defaults()?;
46
47 Ok(Self { d })
49 }
50}
51
52const DEFAULT_IMAGE: &str = "ghcr.io/ledgerhq/speculos";
53
54#[async_trait]
56impl Driver for DockerDriver {
57 type Handle = DockerHandle;
58
59 async fn run(&self, app: &str, opts: Options) -> anyhow::Result<Self::Handle> {
60 let name = format!("speculos-{}", opts.http_port);
62 let create_options = Some(CreateContainerOptions { name: &name });
63
64 let mut ports = vec![opts.http_port];
66 if let Some(p) = opts.apdu_port {
67 ports.push(p);
68 }
69
70 let exposed_ports = ports.iter().map(|p| {
71 let b = PortBinding {
72 host_port: Some(format!("{p}/tcp")),
73 ..Default::default()
74 };
75 (format!("{p}/tcp"), vec![b], HashMap::<(), ()>::new())
76 });
77
78 let app_path = PathBuf::from(app);
79 let app_file = app_path.file_name().and_then(|n| n.to_str()).unwrap();
80
81 let mut cmd = vec![];
83 cmd.append(&mut opts.args());
84 cmd.push(format!("/app/{app_file}"));
85
86 debug!("command: {}", cmd.join(" "));
87
88 let create_config = Config {
90 image: Some(DEFAULT_IMAGE.to_string()),
91 cmd: Some(cmd),
92 attach_stdout: Some(true),
93 attach_stderr: Some(true),
94 stop_signal: Some("KILL".to_string()),
95 exposed_ports: Some(HashMap::from_iter(
96 exposed_ports.clone().map(|p| (p.0, p.2)),
97 )),
98 host_config: Some(HostConfig {
99 port_bindings: Some(HashMap::from_iter(exposed_ports.map(|p| (p.0, Some(p.1))))),
100 ..Default::default()
101 }),
102 ..Default::default()
103 };
104
105 let _ = self
107 .d
108 .remove_container(
109 &name,
110 Some(RemoveContainerOptions {
111 force: true,
112 ..Default::default()
113 }),
114 )
115 .await;
116
117 debug!("Creating container {}", name);
119 let _create_info = self
120 .d
121 .create_container(create_options, create_config)
122 .await?;
123
124 let mut buff = BytesMut::new();
126 let mut tar = tar::Builder::new((&mut buff).writer());
127
128 tar.append_path_with_name(&app_path, format!("app/{app_file}"))?;
129
130 tar.finish()?;
131 drop(tar);
132
133 let upload_options = UploadToContainerOptions {
135 path: "/",
136 ..Default::default()
137 };
138 self.d
139 .upload_to_container(&name, Some(upload_options), buff.to_vec().into())
140 .await?;
141
142 debug!("Starting container {}", name);
144 let _start_info = self
145 .d
146 .start_container(&name, None::<StartContainerOptions<String>>)
147 .await?;
148
149 debug!("Container started");
150
151 let (exit_tx, mut exit_rx) = channel();
152
153 let mut logs = self.d.logs::<String>(
155 &name,
156 Some(LogsOptions {
157 stderr: true,
158 stdout: true,
159 follow: true,
160 ..Default::default()
161 }),
162 );
163
164 tokio::task::spawn(async move {
165 debug!("start log task");
166
167 loop {
168 tokio::select! {
169 l = logs.next() => {
171 match l {
172 Some(Ok(v)) => print!("{v}"),
173 Some(Err(e)) => {
174 debug!("exit log task: {:?}", e);
175 break;
176 },
177 _ => continue,
178 }
179 },
180 _ = &mut exit_rx => {
182 break;
183 }
184 }
185 }
186 });
187
188 let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), opts.http_port);
190 Ok(DockerHandle {
191 name,
192 addr,
193 exit_tx,
194 })
195 }
196
197 async fn wait(&self, handle: &mut Self::Handle) -> anyhow::Result<()> {
198 use ContainerStateStatusEnum::*;
199
200 debug!("Awaiting container completion");
201
202 loop {
204 let info = self.d.inspect_container(&handle.name, None).await?;
206
207 debug!("info: {:?}", info);
208
209 match info.state.and_then(|s| s.status) {
211 Some(CREATED) | Some(RUNNING) => (),
212 Some(_) => return Ok(()),
213 _ => (),
214 }
215
216 tokio::time::sleep(Duration::from_secs(1)).await;
218 }
219 }
220
221 async fn exit(&self, handle: Self::Handle) -> anyhow::Result<()> {
222 debug!("Stopping container {}", handle.name);
224
225 let _ = handle.exit_tx.send(());
227
228 let options = Some(StopContainerOptions { t: 0 });
230 let _ = self.d.stop_container(&handle.name, options).await;
231
232 debug!("Removing container");
234 let options = Some(RemoveContainerOptions {
235 force: true,
236 ..Default::default()
237 });
238 self.d.remove_container(&handle.name, options).await?;
239
240 debug!("Container removed");
241
242 Ok(())
243 }
244}
245
246#[async_trait]
247impl Handle for DockerHandle {
248 fn addr(&self) -> SocketAddr {
249 self.addr
250 }
251}