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    /// Bytes to write to the child's standard input, for the few commands that
37    /// read a document rather than a flag.
38    pub stdin: Option<Vec<u8>>,
39}
40
41impl Invocation {
42    /// A read: overlaps freely, default timeout.
43    pub fn read<I, S>(args: I) -> Self
44    where
45        I: IntoIterator<Item = S>,
46        S: Into<String>,
47    {
48        Self {
49            args: args.into_iter().map(Into::into).collect(),
50            concurrency: Concurrency::Shared,
51            timeout: crate::exec::DEFAULT_TIMEOUT,
52            stdin: None,
53        }
54    }
55
56    /// A mutation: queues behind other mutations.
57    pub fn mutate<I, S>(args: I) -> Self
58    where
59        I: IntoIterator<Item = S>,
60        S: Into<String>,
61    {
62        Self {
63            concurrency: Concurrency::Exclusive,
64            ..Self::read(args)
65        }
66    }
67
68    /// A mutation that cannot race the node's configuration: it changes the
69    /// filesystem, or a peer, but never what `tailscale set` and `tailscale up`
70    /// contend over.
71    ///
72    /// It runs in the shared lane because the lock exists to keep two
73    /// configuration writes apart, and these are not that. Queueing them would
74    /// buy no safety and cost a great deal: a ten-minute file transfer holding
75    /// the exclusive lock stalls every concurrent read for its whole duration.
76    pub fn mutate_shared<I, S>(args: I) -> Self
77    where
78        I: IntoIterator<Item = S>,
79        S: Into<String>,
80    {
81        Self::read(args)
82    }
83
84    #[must_use]
85    pub fn with_timeout(mut self, timeout: Duration) -> Self {
86        self.timeout = timeout;
87        self
88    }
89
90    #[must_use]
91    pub fn with_stdin(mut self, stdin: impl Into<Vec<u8>>) -> Self {
92        self.stdin = Some(stdin.into());
93        self
94    }
95
96    /// The command as it would be written, for logs and error messages. Never
97    /// used to run anything.
98    pub fn display(&self) -> String {
99        let mut out = String::from("tailscale");
100        for arg in &self.args {
101            out.push(' ');
102            out.push_str(arg);
103        }
104        out
105    }
106}
107
108/// What came back.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct Output {
111    /// `None` when the child was terminated by a signal.
112    pub exit_code: Option<i32>,
113    pub stdout: Vec<u8>,
114    pub stderr: String,
115}
116
117impl Output {
118    pub fn success(&self) -> bool {
119        self.exit_code == Some(0)
120    }
121
122    pub fn stdout_str(&self) -> std::borrow::Cow<'_, str> {
123        String::from_utf8_lossy(&self.stdout)
124    }
125}
126
127/// The local node, as the tools see it.
128///
129/// One method, so that a fake is a dozen lines. `dyn`-compatible by hand-rolling
130/// the boxed future rather than taking an `async fn` in the trait.
131pub trait LocalBackend: Send + Sync + std::fmt::Debug {
132    fn run<'a>(&'a self, invocation: Invocation) -> BoxFuture<'a, Result<Output, ExecError>>;
133}
134
135impl<T: LocalBackend + ?Sized> LocalBackend for std::sync::Arc<T> {
136    fn run<'a>(&'a self, invocation: Invocation) -> BoxFuture<'a, Result<Output, ExecError>> {
137        (**self).run(invocation)
138    }
139}
140
141/// A backend that has no binary to run.
142///
143/// Used when the local surface is switched off, or when discovery found
144/// nothing. Every call fails the same way, which keeps the "surface absent"
145/// path identical whether the operator disabled it or the machine lacks the
146/// binary — and means no caller has to special-case an absent backend.
147#[derive(Debug, Clone)]
148pub struct Unavailable {
149    reason: String,
150}
151
152impl Unavailable {
153    pub fn new(reason: impl Into<String>) -> Self {
154        Self {
155            reason: reason.into(),
156        }
157    }
158}
159
160impl Default for Unavailable {
161    fn default() -> Self {
162        Self::new("the local surface is not available")
163    }
164}
165
166impl LocalBackend for Unavailable {
167    fn run<'a>(&'a self, _invocation: Invocation) -> BoxFuture<'a, Result<Output, ExecError>> {
168        Box::pin(async move {
169            Err(ExecError::BinaryNotFound {
170                searched: vec![self.reason.clone()],
171            })
172        })
173    }
174}