ledger_sim/drivers/
docker.rs

1//! Docker driver for speculos execution, runs a speculos instance within
2//! a Docker container.
3
4use 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
28/// Docker-based Speculos driver
29pub struct DockerDriver {
30    d: Docker,
31}
32
33/// Handle to a Speculos instance running under Docker
34#[derive(Debug)]
35pub struct DockerHandle {
36    name: String,
37    addr: SocketAddr,
38    exit_tx: Sender<()>,
39}
40
41impl DockerDriver {
42    /// Create a new docker driver
43    pub fn new() -> Result<Self, anyhow::Error> {
44        // Connect to docker instance
45        let d = Docker::connect_with_local_defaults()?;
46
47        // Return driver
48        Ok(Self { d })
49    }
50}
51
52const DEFAULT_IMAGE: &str = "ghcr.io/ledgerhq/speculos";
53
54/// [Driver] implementation for [DockerDriver]
55#[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        // Set container name
61        let name = format!("speculos-{}", opts.http_port);
62        let create_options = Some(CreateContainerOptions { name: &name });
63
64        // Setup ports
65        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        // Setup speculos command
82        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        // Setup container
89        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        // Remove existing container if there is one
106        let _ = self
107            .d
108            .remove_container(
109                &name,
110                Some(RemoveContainerOptions {
111                    force: true,
112                    ..Default::default()
113                }),
114            )
115            .await;
116
117        // Create container
118        debug!("Creating container {}", name);
119        let _create_info = self
120            .d
121            .create_container(create_options, create_config)
122            .await?;
123
124        // Generate application archive
125        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        // Write app archive to container
134        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        // Start container
143        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        // Setup log streaming task
154        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                    // Fetch log entries
170                    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                    // Handle exit signal
181                    _ = &mut exit_rx => {
182                        break;
183                    }
184                }
185            }
186        });
187
188        // Return container handle
189        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        // Poll container info periodically
203        loop {
204            // Fetch container info
205            let info = self.d.inspect_container(&handle.name, None).await?;
206
207            debug!("info: {:?}", info);
208
209            // Return when container exits
210            match info.state.and_then(|s| s.status) {
211                Some(CREATED) | Some(RUNNING) => (),
212                Some(_) => return Ok(()),
213                _ => (),
214            }
215
216            // Sleep for a while
217            tokio::time::sleep(Duration::from_secs(1)).await;
218        }
219    }
220
221    async fn exit(&self, handle: Self::Handle) -> anyhow::Result<()> {
222        // Stop container
223        debug!("Stopping container {}", handle.name);
224
225        // Send exit signal to log task
226        let _ = handle.exit_tx.send(());
227
228        // Send container stop signal
229        let options = Some(StopContainerOptions { t: 0 });
230        let _ = self.d.stop_container(&handle.name, options).await;
231
232        // Remove container
233        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}