Skip to main content

lager/
client.rs

1//! Blocking client for the Lager box HTTP API (default feature).
2
3use std::time::Duration;
4
5use serde_json::Value;
6
7use crate::auth::{self, GatewayAuth};
8use crate::error::{Error, Result};
9use crate::nets::adc::Adc;
10use crate::nets::arm::Arm;
11use crate::nets::battery::Battery;
12use crate::nets::ble::Ble;
13use crate::nets::blufi::Blufi;
14use crate::nets::dac::Dac;
15use crate::nets::debug::DebugNet;
16use crate::nets::dfu::Dfu;
17use crate::nets::eload::Eload;
18use crate::nets::energy::EnergyAnalyzer;
19use crate::nets::gpio::Gpio;
20use crate::nets::i2c::I2c;
21use crate::nets::router::Router;
22use crate::nets::scope::Scope;
23use crate::nets::solar::Solar;
24use crate::nets::spi::Spi;
25use crate::nets::supply::Supply;
26use crate::nets::thermocouple::Thermocouple;
27use crate::nets::usb::UsbPort;
28use crate::nets::watt::WattMeter;
29use crate::nets::webcam::Webcam;
30use crate::nets::wifi::Wifi;
31use crate::wire::{
32    self, BoxLock, BoxStatus, Health, HttpRequest, Method, NetRecord, Op, Timeout,
33    UsbDeviceFilter, UsbDeviceInfo,
34};
35use crate::BOX_HOST_ENV;
36
37/// A connection to one Lager box, over blocking HTTP.
38///
39/// Cheap to construct: no network traffic happens until the first call.
40/// Net handles borrow the client, so a typical test creates one `LagerBox`
41/// and any number of handles from it.
42///
43/// ```no_run
44/// use lager::LagerBox;
45///
46/// fn main() -> lager::Result<()> {
47///     let lager = LagerBox::connect("192.168.1.42")?;
48///     let supply = lager.supply("supply1");
49///     supply.set_voltage(3.3)?;
50///     supply.enable()?;
51///     let v = lager.adc("vbat_sense").read()?;
52///     assert!((v - 3.3).abs() < 0.1);
53///     supply.disable()
54/// }
55/// ```
56pub struct LagerBox {
57    base: String,
58    debug_base: String,
59    agent: ureq::Agent,
60    default_timeout: Duration,
61    auth: GatewayAuth,
62}
63
64/// Builder for [`LagerBox`], for overriding the default timeout, the
65/// debug-service URL, and gateway auth.
66pub struct LagerBoxBuilder {
67    host: String,
68    debug_url: Option<String>,
69    default_timeout: Duration,
70    bearer_token: Option<String>,
71}
72
73impl LagerBoxBuilder {
74    /// Override the default HTTP timeout for quick commands (10s unless
75    /// changed). Long-running actions (watt/energy windows,
76    /// `wait_for_level`) still compute their own wider budgets.
77    pub fn timeout(mut self, timeout: Duration) -> Self {
78        self.default_timeout = timeout;
79        self
80    }
81
82    /// Override the debug-service base URL (default: the box host on port
83    /// 8765). Use this when the debug service is reached through an SSH
84    /// tunnel (e.g. `http://127.0.0.1:8765`). Also settable via the
85    /// `LAGER_DEBUG_SERVICE_URL` environment variable.
86    pub fn debug_service_url(mut self, url: impl Into<String>) -> Self {
87        self.debug_url = Some(url.into());
88        self
89    }
90
91    /// Attach `Authorization: Bearer <token>` to every request, for boxes
92    /// behind an authenticating gateway. Also settable via the
93    /// `LAGER_GATEWAY_TOKEN` environment variable.
94    ///
95    /// Without this, the crate reuses the Lager CLI's session
96    /// (`lager login <auth_url>`, stored in `~/.lager_gateway_auth`)
97    /// automatically when a gateway asks for auth, including transparent
98    /// refresh of expired access tokens. Plain (ungated) boxes are
99    /// unaffected either way.
100    pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
101        self.bearer_token = Some(token.into());
102        self
103    }
104
105    /// Build the client.
106    pub fn build(self) -> Result<LagerBox> {
107        let base = wire::base_url(&self.host)?;
108        let debug_base = match self.debug_url {
109            Some(url) => wire::base_url_with_port(&url, wire::DEBUG_SERVICE_PORT)?,
110            None => wire::service_base(&base, wire::DEBUG_SERVICE_PORT),
111        };
112        let auth = GatewayAuth::new(&base, self.bearer_token);
113        Ok(LagerBox {
114            base,
115            debug_base,
116            agent: ureq::AgentBuilder::new().build(),
117            default_timeout: self.default_timeout,
118            auth,
119        })
120    }
121}
122
123impl LagerBox {
124    /// Connect to a box by host name, IP, `host:port`, or full URL.
125    /// The port defaults to 9000 (the box HTTP server).
126    pub fn connect(host: impl Into<String>) -> Result<Self> {
127        Self::builder(host).build()
128    }
129
130    /// Connect to the box named by the `LAGER_BOX_HOST` environment
131    /// variable.
132    pub fn from_env() -> Result<Self> {
133        let host = std::env::var(BOX_HOST_ENV)
134            .map_err(|_| Error::Config(format!("{BOX_HOST_ENV} is not set")))?;
135        Self::connect(host)
136    }
137
138    /// Start building a client with non-default settings.
139    pub fn builder(host: impl Into<String>) -> LagerBoxBuilder {
140        LagerBoxBuilder {
141            host: host.into(),
142            debug_url: std::env::var(crate::DEBUG_SERVICE_URL_ENV).ok(),
143            default_timeout: wire::DEFAULT_TIMEOUT,
144            bearer_token: None,
145        }
146    }
147
148    /// The base URL this client talks to, e.g. `http://192.168.1.42:9000`.
149    pub fn base_url(&self) -> &str {
150        &self.base
151    }
152
153    // -- transport ---------------------------------------------------------
154
155    /// Send one request against the port-9000 server.
156    pub(crate) fn execute(&self, req: &HttpRequest) -> Result<(u16, Value)> {
157        self.execute_at(&self.base, req)
158    }
159
160    /// Send one request against the debug service (port 8765).
161    pub(crate) fn execute_debug(&self, req: &HttpRequest) -> Result<(u16, Value)> {
162        self.execute_at(&self.debug_base, req)
163    }
164
165    /// Send one request against an arbitrary base URL and return
166    /// `(status, parsed JSON body)`. Attaches gateway auth when known, and
167    /// handles a gateway denial by resolving credentials (CLI session
168    /// store, with transparent refresh) and retrying once.
169    fn execute_at(&self, base: &str, req: &HttpRequest) -> Result<(u16, Value)> {
170        let token = self.current_token();
171        let (status, gateway, resp_body) = self.send_once(base, req, token.as_deref())?;
172
173        let Some(auth_url) = gateway else {
174            return Ok((status, resp_body));
175        };
176        // Gateway denial: learn the box→auth-server mapping (like the CLI),
177        // then retry once with a credential the gateway has not just seen.
178        self.auth.learn_auth_server(&auth_url);
179        if status == 401 && !self.auth.has_static_token() {
180            if let Some(fresh) = self
181                .auth
182                .resolve_token_blocking(&auth_url, token.as_deref())
183            {
184                let (status, gateway, resp_body) =
185                    self.send_once(base, req, Some(&fresh))?;
186                let Some(auth_url) = gateway else {
187                    return Ok((status, resp_body));
188                };
189                return Err(auth::denial_error(
190                    status,
191                    self.auth.box_host(),
192                    &auth_url,
193                    true,
194                ));
195            }
196        }
197        Err(auth::denial_error(
198            status,
199            self.auth.box_host(),
200            &auth_url,
201            token.is_some(),
202        ))
203    }
204
205    /// Token to attach right now: builder/env token, cached session token,
206    /// or a store lookup when the box is already known to be gated.
207    fn current_token(&self) -> Option<String> {
208        if let Some(token) = self.auth.cached_token() {
209            return Some(token);
210        }
211        if self.auth.wants_store_token() {
212            let auth_url = self.auth.auth_url()?;
213            return self.auth.resolve_token_blocking(&auth_url, None);
214        }
215        None
216    }
217
218    /// One HTTP round-trip. Returns `(status, gateway_denial_auth_url,
219    /// body)`; the auth URL is `Some` only for a gateway denial (401/403/
220    /// 503 carrying the discovery header).
221    fn send_once(
222        &self,
223        base: &str,
224        req: &HttpRequest,
225        token: Option<&str>,
226    ) -> Result<(u16, Option<String>, Value)> {
227        let url = format!("{}{}", base, req.path);
228        let mut r = match req.method {
229            Method::Get => self.agent.request("GET", &url),
230            Method::Post => self.agent.request("POST", &url),
231        };
232        match req.timeout {
233            Timeout::Default => r = r.timeout(self.default_timeout),
234            Timeout::After(d) => r = r.timeout(d),
235            Timeout::Unbounded => {}
236        }
237        if let Some(token) = token {
238            r = r.set("Authorization", &format!("Bearer {token}"));
239        }
240        let outcome = match (&req.method, &req.body) {
241            (Method::Post, Some(body)) => r.send_json(body.clone()),
242            _ => r.call(),
243        };
244        let resp = match outcome {
245            Ok(resp) => resp,
246            // ureq reports 4xx/5xx as Err(Status); the box still sends a
247            // JSON error body we need to surface.
248            Err(ureq::Error::Status(_, resp)) => resp,
249            Err(ureq::Error::Transport(t)) => {
250                let msg = t.to_string();
251                return if msg.to_ascii_lowercase().contains("timed out")
252                    || msg.to_ascii_lowercase().contains("timeout")
253                {
254                    Err(Error::Timeout(msg))
255                } else {
256                    Err(Error::Connection(msg))
257                };
258            }
259        };
260        let status = resp.status();
261        let gateway = if auth::is_denial(status) {
262            resp.header(auth::DISCOVERY_HEADER).map(str::to_string)
263        } else {
264            None
265        };
266        let body: Value = match resp.into_json() {
267            Ok(body) => body,
268            Err(e) if status < 400 => {
269                return Err(Error::Decode(format!("non-JSON response: {e}")))
270            }
271            // Error responses (incl. gateway denials) may have no JSON body.
272            Err(_) => Value::Null,
273        };
274        if gateway.is_none() && status >= 400 && body.is_null() {
275            return Err(Error::Box {
276                status,
277                message: format!("HTTP {status} (non-JSON body)"),
278            });
279        }
280        Ok((status, gateway, body))
281    }
282
283    /// Execute one typed operation against a command endpoint.
284    pub(crate) fn run<T>(&self, op: Op<T>) -> Result<T> {
285        let (status, body) = self.execute(&op.req)?;
286        let resp = wire::parse_command(status, body)?;
287        (op.parse)(resp)
288    }
289
290    /// Open a streaming response body against the debug service (used by
291    /// RTT). Returns the raw byte reader.
292    pub(crate) fn stream_debug(
293        &self,
294        req: &HttpRequest,
295    ) -> Result<Box<dyn std::io::Read + Send + Sync>> {
296        let token = self.current_token();
297        match self.stream_debug_once(req, token.as_deref()) {
298            // Gateway denial (only a 401 maps to AuthRequired): resolve
299            // credentials from the CLI session store — avoiding the token
300            // the gateway just rejected — and retry once, like execute_at.
301            Err(Error::AuthRequired {
302                box_host,
303                auth_url,
304                message,
305            }) if !self.auth.has_static_token() => {
306                match self
307                    .auth
308                    .resolve_token_blocking(&auth_url, token.as_deref())
309                {
310                    Some(fresh) => self.stream_debug_once(req, Some(&fresh)),
311                    None => Err(Error::AuthRequired {
312                        box_host,
313                        auth_url,
314                        message,
315                    }),
316                }
317            }
318            other => other,
319        }
320    }
321
322    fn stream_debug_once(
323        &self,
324        req: &HttpRequest,
325        token: Option<&str>,
326    ) -> Result<Box<dyn std::io::Read + Send + Sync>> {
327        let url = format!("{}{}", self.debug_base, req.path);
328        let mut r = self.agent.request("POST", &url);
329        match req.timeout {
330            Timeout::Default => r = r.timeout(self.default_timeout),
331            Timeout::After(d) => r = r.timeout(d),
332            Timeout::Unbounded => {}
333        }
334        if let Some(token) = token {
335            r = r.set("Authorization", &format!("Bearer {token}"));
336        }
337        let outcome = match &req.body {
338            Some(body) => r.send_json(body.clone()),
339            None => r.call(),
340        };
341        match outcome {
342            Ok(resp) => Ok(resp.into_reader()),
343            Err(ureq::Error::Status(status, resp)) => {
344                if auth::is_denial(status) {
345                    if let Some(auth_url) = resp.header(auth::DISCOVERY_HEADER) {
346                        let auth_url = auth_url.to_string();
347                        self.auth.learn_auth_server(&auth_url);
348                        return Err(auth::denial_error(
349                            status,
350                            self.auth.box_host(),
351                            &auth_url,
352                            token.is_some(),
353                        ));
354                    }
355                }
356                let body: Value = resp.into_json().unwrap_or(Value::Null);
357                Err(wire::parse_debug(status, body).unwrap_err())
358            }
359            Err(ureq::Error::Transport(t)) => Err(Error::Connection(t.to_string())),
360        }
361    }
362
363    /// Resolve a debug net's full saved record (needed by the debug service,
364    /// which reads probe fields out of the record).
365    pub(crate) fn debug_net_record(&self, name: &str) -> Result<Value> {
366        let records = self.nets_raw()?;
367        crate::nets::debug::find_debug_record(records, name)
368    }
369
370    /// Raw saved-net records (untyped), with the same `/nets/list` ->
371    /// `/uart/nets/list` fallback as [`LagerBox::nets`].
372    fn nets_raw(&self) -> Result<Vec<Value>> {
373        let body = match self.get_json("/nets/list") {
374            Ok(body) => body,
375            Err(primary) => self.get_json("/uart/nets/list").map_err(|_| primary)?,
376        };
377        Ok(wire::nets_list_values(body))
378    }
379
380    fn get_json(&self, path: &str) -> Result<Value> {
381        let (status, body) = self.execute(&wire::get(path))?;
382        if status != 200 {
383            return Err(Error::Box {
384                status,
385                message: body
386                    .get("error")
387                    .and_then(Value::as_str)
388                    .unwrap_or("request failed")
389                    .to_string(),
390            });
391        }
392        Ok(body)
393    }
394
395    // -- box-level queries --------------------------------------------------
396
397    /// List every net configured on the box (full saved records).
398    ///
399    /// Falls back to the older `/uart/nets/list` shape for box images that
400    /// predate `/nets/list`, like the Lager CLI does.
401    pub fn nets(&self) -> Result<Vec<NetRecord>> {
402        match self.get_json("/nets/list") {
403            Ok(body) => wire::nets_from_body(body),
404            Err(primary) => match self.get_json("/uart/nets/list") {
405                Ok(body) => wire::nets_from_body(body),
406                Err(_) => Err(primary),
407            },
408        }
409    }
410
411    /// Check that the box HTTP server is up.
412    pub fn health(&self) -> Result<Health> {
413        let body = self.get_json("/health")?;
414        serde_json::from_value(body).map_err(Into::into)
415    }
416
417    /// Box status: version, configured nets, and endpoint capabilities.
418    pub fn status(&self) -> Result<BoxStatus> {
419        let body = self.get_json("/status")?;
420        serde_json::from_value(body).map_err(Into::into)
421    }
422
423    /// Enumerate USB devices on the box's bus from sysfs (lsusb-like).
424    ///
425    /// A few milliseconds per call with no exclusive device access, so it
426    /// is safe to poll frequently — e.g. reading the DUT's iSerial to see
427    /// what it re-enumerated as after a hub power-cycle or DFU detach.
428    ///
429    /// Requires box software >= 0.33.0; older boxes fail with
430    /// [`Error::UnsupportedByBox`].
431    pub fn usb_devices(&self) -> Result<Vec<UsbDeviceInfo>> {
432        self.usb_devices_matching(&UsbDeviceFilter::default())
433    }
434
435    /// Like [`LagerBox::usb_devices`], with box-side vid/pid/serial
436    /// filters.
437    pub fn usb_devices_matching(&self, filter: &UsbDeviceFilter) -> Result<Vec<UsbDeviceInfo>> {
438        match self.execute(&wire::usb_devices(filter)) {
439            Ok((status, body)) => wire::parse_usb_devices(status, body),
440            Err(e) => Err(wire::map_route_missing(e, wire::usb_devices_unsupported)),
441        }
442    }
443
444    // -- box lock / reservation ----------------------------------------------
445
446    fn lock_call(&self, req: &HttpRequest) -> Result<BoxLock> {
447        match self.execute(req) {
448            Ok((status, body)) => wire::parse_lock(status, body),
449            Err(e) => Err(wire::map_route_missing(e, wire::lock_unsupported)),
450        }
451    }
452
453    /// Current box lock state (`GET /lock`); `locked: false` when free.
454    pub fn lock_status(&self) -> Result<BoxLock> {
455        self.lock_call(&wire::lock_status())
456    }
457
458    /// Claim the box for `user` (an eternal `holder_type: "user"` lock,
459    /// exactly like `lager boxes lock`). Re-acquiring your own lock
460    /// refreshes it; a box held by someone else fails with
461    /// [`Error::Box`] (HTTP 409) naming the holder.
462    pub fn lock(&self, user: &str) -> Result<BoxLock> {
463        self.lock_call(&wire::lock_acquire(user, "user", None))
464    }
465
466    /// Claim the box with an explicit holder type and TTL.
467    /// `ttl_seconds: None` means the lock never auto-expires; with a TTL,
468    /// keep the lock alive via [`LagerBox::lock_heartbeat`].
469    pub fn lock_with(
470        &self,
471        user: &str,
472        holder_type: &str,
473        ttl_seconds: Option<u64>,
474    ) -> Result<BoxLock> {
475        self.lock_call(&wire::lock_acquire(user, holder_type, ttl_seconds))
476    }
477
478    /// Refresh a TTL lock's heartbeat. Fails with [`Error::Box`] when the
479    /// box is not locked (HTTP 404) or held by someone else (HTTP 403).
480    pub fn lock_heartbeat(&self, user: &str) -> Result<BoxLock> {
481        self.lock_call(&wire::lock_heartbeat(user))
482    }
483
484    /// Release `user`'s box lock. Releasing an already-unlocked box
485    /// succeeds; a box held by someone else fails with [`Error::Box`]
486    /// (HTTP 403).
487    pub fn unlock(&self, user: &str) -> Result<()> {
488        self.lock_call(&wire::unlock(user, false)).map(|_| ())
489    }
490
491    /// Release the box lock even when held by another user
492    /// (`lager boxes unlock --force`).
493    pub fn unlock_force(&self, user: &str) -> Result<()> {
494        self.lock_call(&wire::unlock(user, true)).map(|_| ())
495    }
496
497    /// Claim the box for `user` and release the claim when the returned
498    /// guard drops (best-effort; call [`BoxLockGuard::unlock`] to surface
499    /// release errors).
500    pub fn lock_guard(&self, user: impl Into<String>) -> Result<BoxLockGuard<'_>> {
501        let user = user.into();
502        self.lock(&user)?;
503        Ok(BoxLockGuard { client: self, user, released: false })
504    }
505
506    // -- net handle constructors ---------------------------------------------
507
508    /// Handle for a power-supply net.
509    pub fn supply(&self, name: impl Into<String>) -> Supply<'_> {
510        Supply { client: self, name: name.into() }
511    }
512
513    /// Handle for a battery-simulator net.
514    pub fn battery(&self, name: impl Into<String>) -> Battery<'_> {
515        Battery { client: self, name: name.into() }
516    }
517
518    /// Handle for an electronic-load net.
519    pub fn eload(&self, name: impl Into<String>) -> Eload<'_> {
520        Eload { client: self, name: name.into() }
521    }
522
523    /// Handle for a solar-simulator net (EA PSB photovoltaic mode).
524    pub fn solar(&self, name: impl Into<String>) -> Solar<'_> {
525        Solar { client: self, name: name.into() }
526    }
527
528    /// Handle for a GPIO net.
529    pub fn gpio(&self, name: impl Into<String>) -> Gpio<'_> {
530        Gpio { client: self, name: name.into() }
531    }
532
533    /// Handle for an ADC net.
534    pub fn adc(&self, name: impl Into<String>) -> Adc<'_> {
535        Adc { client: self, name: name.into() }
536    }
537
538    /// Handle for a DAC net.
539    pub fn dac(&self, name: impl Into<String>) -> Dac<'_> {
540        Dac { client: self, name: name.into() }
541    }
542
543    /// Handle for a thermocouple net.
544    pub fn thermocouple(&self, name: impl Into<String>) -> Thermocouple<'_> {
545        Thermocouple { client: self, name: name.into() }
546    }
547
548    /// Handle for a watt-meter net.
549    pub fn watt_meter(&self, name: impl Into<String>) -> WattMeter<'_> {
550        WattMeter { client: self, name: name.into() }
551    }
552
553    /// Handle for an energy-analyzer net.
554    pub fn energy_analyzer(&self, name: impl Into<String>) -> EnergyAnalyzer<'_> {
555        EnergyAnalyzer { client: self, name: name.into() }
556    }
557
558    /// Handle for an SPI bus net.
559    pub fn spi(&self, name: impl Into<String>) -> Spi<'_> {
560        Spi { client: self, name: name.into() }
561    }
562
563    /// Handle for an I2C bus net.
564    pub fn i2c(&self, name: impl Into<String>) -> I2c<'_> {
565        I2c { client: self, name: name.into() }
566    }
567
568    /// Handle for a USB hub port net.
569    pub fn usb(&self, name: impl Into<String>) -> UsbPort<'_> {
570        UsbPort { client: self, name: name.into() }
571    }
572
573    /// Handle for a robot-arm net (Rotrics Dexarm).
574    pub fn arm(&self, name: impl Into<String>) -> Arm<'_> {
575        Arm { client: self, name: name.into() }
576    }
577
578    /// Handle for a webcam net (MJPEG streaming).
579    pub fn webcam(&self, name: impl Into<String>) -> Webcam<'_> {
580        Webcam { client: self, name: name.into() }
581    }
582
583    /// Handle for a router net (MikroTik RouterOS).
584    pub fn router(&self, name: impl Into<String>) -> Router<'_> {
585        Router { client: self, name: name.into() }
586    }
587
588    /// Handle for the box's BLE adapter (box-level, not a saved net).
589    pub fn ble(&self) -> Ble<'_> {
590        Ble { client: self }
591    }
592
593    /// Handle for the box's WiFi interface (box-level, not a saved net).
594    pub fn wifi(&self) -> Wifi<'_> {
595        Wifi { client: self }
596    }
597
598    /// Handle for BluFi (ESP32 WiFi provisioning over BLE; box-level).
599    pub fn blufi(&self) -> Blufi<'_> {
600        Blufi { client: self }
601    }
602
603    /// Handle for box-side DFU via dfu-util (box-level, not a saved net).
604    pub fn dfu(&self) -> Dfu<'_> {
605        Dfu { client: self }
606    }
607
608    /// Handle for a debug-probe net (flash/erase/reset/read_memory/RTT).
609    /// Talks to the box debug service on port 8765.
610    pub fn debug(&self, name: impl Into<String>) -> DebugNet<'_> {
611        DebugNet { client: self, name: name.into(), record: Default::default() }
612    }
613
614    /// Handle for an oscilloscope net. **Stub:** see [`Scope`].
615    pub fn scope(&self, name: impl Into<String>) -> Scope {
616        Scope::new(name)
617    }
618
619    /// Open a streaming UART session on a UART net.
620    ///
621    /// Connects a Socket.IO session to the box's `/uart` namespace and
622    /// starts streaming. Only one session per net (box-enforced).
623    #[cfg(feature = "uart")]
624    pub fn uart(&self, name: impl Into<String>) -> Result<crate::nets::uart::Uart> {
625        crate::nets::uart::Uart::open(&self.base, name.into(), self.current_token())
626    }
627}
628
629/// RAII box-lock claim from [`LagerBox::lock_guard`]: releases the lock on
630/// drop (best-effort — a drop cannot surface errors, so tests that must
631/// know the release worked should call [`BoxLockGuard::unlock`]).
632pub struct BoxLockGuard<'a> {
633    client: &'a LagerBox,
634    user: String,
635    released: bool,
636}
637
638impl BoxLockGuard<'_> {
639    /// The user holding this claim.
640    pub fn user(&self) -> &str {
641        &self.user
642    }
643
644    /// Release the lock now, surfacing any error.
645    pub fn unlock(mut self) -> Result<()> {
646        self.released = true;
647        self.client.unlock(&self.user)
648    }
649}
650
651impl Drop for BoxLockGuard<'_> {
652    fn drop(&mut self) {
653        if !self.released {
654            let _ = self.client.unlock(&self.user);
655        }
656    }
657}