Skip to main content

tailscale_cli/
backend.rs

1//! What the rest of the server sees of the local node.
2
3use std::future::Future;
4use std::pin::Pin;
5use std::time::Duration;
6
7use crate::exec::ExecError;
8
9/// A boxed future, spelled out rather than pulled from `futures`, because this
10/// crate needs exactly one of them.
11pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
12
13/// Whether a call may overlap with others.
14///
15/// The local node serialises its own mutations anyway, but two `tailscale set`
16/// calls racing produce a result neither caller asked for, and the failure is
17/// invisible. Mutating calls therefore queue.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Concurrency {
20    /// Runs alongside any number of other reads.
21    Shared,
22    /// Runs alone.
23    Exclusive,
24}
25
26/// One invocation of the CLI.
27///
28/// The argument list is a list. It is never joined into a string and never
29/// handed to a shell, which is what makes an argument containing a space, a
30/// quote or a semicolon uninteresting.
31#[derive(Debug, Clone)]
32pub struct Invocation {
33    pub args: Vec<String>,
34    pub concurrency: Concurrency,
35    pub timeout: Duration,
36}
37
38impl Invocation {
39    /// A read: overlaps freely, default timeout.
40    pub fn read<I, S>(args: I) -> Self
41    where
42        I: IntoIterator<Item = S>,
43        S: Into<String>,
44    {
45        Self {
46            args: args.into_iter().map(Into::into).collect(),
47            concurrency: Concurrency::Shared,
48            timeout: crate::exec::DEFAULT_TIMEOUT,
49        }
50    }
51
52    /// A mutation: queues behind other mutations.
53    pub fn mutate<I, S>(args: I) -> Self
54    where
55        I: IntoIterator<Item = S>,
56        S: Into<String>,
57    {
58        Self {
59            concurrency: Concurrency::Exclusive,
60            ..Self::read(args)
61        }
62    }
63
64    /// A mutation that cannot race the node's configuration: it changes the
65    /// filesystem, or a peer, but never what `tailscale set` and `tailscale up`
66    /// contend over.
67    ///
68    /// It runs in the shared lane because the lock exists to keep two
69    /// configuration writes apart, and these are not that. Queueing them would
70    /// buy no safety and cost a great deal: a ten-minute file transfer holding
71    /// the exclusive lock stalls every concurrent read for its whole duration.
72    pub fn mutate_shared<I, S>(args: I) -> Self
73    where
74        I: IntoIterator<Item = S>,
75        S: Into<String>,
76    {
77        Self::read(args)
78    }
79
80    #[must_use]
81    pub fn with_timeout(mut self, timeout: Duration) -> Self {
82        self.timeout = timeout;
83        self
84    }
85
86    /// The command as it would be written, for logs and error messages. Never
87    /// used to run anything.
88    pub fn display(&self) -> String {
89        let mut out = String::from("tailscale");
90        for arg in &self.args {
91            out.push(' ');
92            out.push_str(arg);
93        }
94        out
95    }
96}
97
98/// What came back.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct Output {
101    /// `None` when the child was terminated by a signal.
102    pub exit_code: Option<i32>,
103    pub stdout: Vec<u8>,
104    pub stderr: String,
105}
106
107impl Output {
108    pub fn success(&self) -> bool {
109        self.exit_code == Some(0)
110    }
111
112    pub fn stdout_str(&self) -> std::borrow::Cow<'_, str> {
113        String::from_utf8_lossy(&self.stdout)
114    }
115}
116
117/// The local node, as the tools see it.
118///
119/// One method, so that a fake is a dozen lines. `dyn`-compatible by hand-rolling
120/// the boxed future rather than taking an `async fn` in the trait.
121pub trait LocalBackend: Send + Sync + std::fmt::Debug {
122    fn run<'a>(&'a self, invocation: Invocation) -> BoxFuture<'a, Result<Output, ExecError>>;
123}
124
125impl<T: LocalBackend + ?Sized> LocalBackend for std::sync::Arc<T> {
126    fn run<'a>(&'a self, invocation: Invocation) -> BoxFuture<'a, Result<Output, ExecError>> {
127        (**self).run(invocation)
128    }
129}
130
131/// A backend that has no binary to run.
132///
133/// Used when the local surface is switched off, or when discovery found
134/// nothing. Every call fails the same way, which keeps the "surface absent"
135/// path identical whether the operator disabled it or the machine lacks the
136/// binary — and means no caller has to special-case an absent backend.
137#[derive(Debug, Clone)]
138pub struct Unavailable {
139    reason: String,
140}
141
142impl Unavailable {
143    pub fn new(reason: impl Into<String>) -> Self {
144        Self {
145            reason: reason.into(),
146        }
147    }
148}
149
150impl Default for Unavailable {
151    fn default() -> Self {
152        Self::new("the local surface is not available")
153    }
154}
155
156impl LocalBackend for Unavailable {
157    fn run<'a>(&'a self, _invocation: Invocation) -> BoxFuture<'a, Result<Output, ExecError>> {
158        Box::pin(async move {
159            Err(ExecError::BinaryNotFound {
160                searched: vec![self.reason.clone()],
161            })
162        })
163    }
164}