1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 05:12:40 +03:00

Move server to its own wezterm-mux-server binary

This commit is contained in:
Wez Furlong 2020-10-02 23:35:18 -07:00
parent a07004302a
commit 0c32963f1c
20 changed files with 454 additions and 133 deletions

35
Cargo.lock generated
View File

@ -651,6 +651,7 @@ name = "codec"
version = "0.1.0"
dependencies = [
"anyhow",
"base91",
"config",
"leb128",
"log",
@ -3711,6 +3712,13 @@ dependencies = [
"ws2_32-sys",
]
[[package]]
name = "umask"
version = "0.1.0"
dependencies = [
"libc",
]
[[package]]
name = "unicase"
version = "2.6.0"
@ -4111,7 +4119,6 @@ dependencies = [
"async-task 1.3.1",
"async-trait",
"base64 0.10.1",
"base91",
"bitflags 1.2.1",
"bstr 0.2.13",
"cc",
@ -4174,6 +4181,7 @@ dependencies = [
"tinyvec",
"toml",
"uds_windows",
"umask",
"unicode-normalization",
"unicode-segmentation",
"unicode-width",
@ -4185,6 +4193,31 @@ dependencies = [
"winrt-notification",
]
[[package]]
name = "wezterm-mux-server"
version = "0.1.0"
dependencies = [
"anyhow",
"codec",
"config",
"crossbeam",
"daemonize",
"filedescriptor",
"hostname",
"log",
"mux",
"openssl",
"portable-pty",
"pretty_env_logger",
"promise",
"rangeset",
"rcgen",
"structopt",
"umask",
"url",
"wezterm-term",
]
[[package]]
name = "wezterm-term"
version = "0.1.0"

View File

@ -24,7 +24,6 @@ async-trait = "0.1"
anyhow = "1.0"
thiserror = "1.0"
base64 = "0.10"
base91 = { path = "base91" }
rangeset = { path = "rangeset" }
bitflags = "1.0"
bstr = "0.2"
@ -72,6 +71,7 @@ termwiz = { path = "termwiz" }
textwrap = "0.11"
tinyvec = "0.3"
toml = "0.4"
umask = { path = "umask" }
unicode-normalization = "0.1"
unicode-segmentation = "1.6"
unicode-width = "0.1"
@ -123,6 +123,7 @@ default = ["vendor_openssl"]
vendor_openssl = ["openssl/vendored"]
[workspace]
members = ["wezterm-mux-server"]
[profile.release]
opt-level = 3

View File

@ -20,3 +20,6 @@ termwiz = { path = "../termwiz" }
varbincode = "0.1"
wezterm-term = { path = "../term", features=["use_serde"] }
zstd = "0.4"
[dev-dependencies]
base91 = { path = "../base91" }

View File

@ -5,8 +5,6 @@ pub enum FrontEndSelection {
OpenGL,
Software,
OldSoftware,
MuxServer,
Null,
}
impl_lua_conversion!(FrontEndSelection);
@ -19,7 +17,7 @@ impl Default for FrontEndSelection {
impl FrontEndSelection {
// TODO: find or build a proc macro for this
pub fn variants() -> Vec<&'static str> {
vec!["OpenGL", "Software", "OldSoftware", "MuxServer", "Null"]
vec!["OpenGL", "Software", "OldSoftware"]
}
}
@ -27,8 +25,6 @@ impl std::str::FromStr for FrontEndSelection {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_ref() {
"muxserver" => Ok(FrontEndSelection::MuxServer),
"null" => Ok(FrontEndSelection::Null),
"software" => Ok(FrontEndSelection::Software),
"oldsoftware" => Ok(FrontEndSelection::OldSoftware),
"opengl" => Ok(FrontEndSelection::OpenGL),

View File

@ -24,9 +24,9 @@ pub struct UnixDomain {
/// If we decide that we need to start the server, the command to run
/// to set that up. The default is to spawn:
/// `wezterm start --daemonize --front-end MuxServer`
/// `wezterm-mux-server --daemonize`
/// but it can be useful to set this to eg:
/// `wsl -e wezterm --daemonize --front-end MuxServer` to start up
/// `wsl -e wezterm-mux-server --daemonize` to start up
/// a unix domain inside a wsl container.
pub serve_command: Option<Vec<String>>,
@ -66,11 +66,10 @@ impl UnixDomain {
match self.serve_command.as_ref() {
Some(cmd) => Ok(cmd.iter().map(Into::into).collect()),
None => Ok(vec![
std::env::current_exe()?.into_os_string(),
OsString::from("start"),
std::env::current_exe()?
.with_file_name("wezterm-mux-server")
.into_os_string(),
OsString::from("--daemonize"),
OsString::from("--front-end"),
OsString::from("MuxServer"),
]),
}
}

View File

@ -5,7 +5,6 @@ use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
pub mod gui;
pub mod muxserver;
pub use config::FrontEndSelection;
@ -37,18 +36,16 @@ pub fn shutdown() {
}
pub fn try_new(sel: FrontEndSelection) -> Result<Rc<dyn FrontEnd>, Error> {
let (front_end, is_gui) = match sel {
FrontEndSelection::MuxServer => (muxserver::MuxServerFrontEnd::try_new(), false),
FrontEndSelection::Null => (muxserver::MuxServerFrontEnd::new_null(), false),
FrontEndSelection::Software => (gui::GuiFrontEnd::try_new_swrast(), true),
FrontEndSelection::OldSoftware => (gui::GuiFrontEnd::try_new_no_opengl(), true),
FrontEndSelection::OpenGL => (gui::GuiFrontEnd::try_new(), true),
let front_end = match sel {
FrontEndSelection::Software => gui::GuiFrontEnd::try_new_swrast(),
FrontEndSelection::OldSoftware => gui::GuiFrontEnd::try_new_no_opengl(),
FrontEndSelection::OpenGL => gui::GuiFrontEnd::try_new(),
};
let front_end = front_end?;
FRONT_END.with(|f| *f.borrow_mut() = Some(Rc::clone(&front_end)));
HAS_GUI_FRONT_END.store(is_gui, Ordering::Release);
HAS_GUI_FRONT_END.store(true, Ordering::Release);
Ok(front_end)
}

View File

@ -1,62 +0,0 @@
//! Implements the multiplexer server frontend
use crate::frontend::FrontEnd;
use crate::server::listener::spawn_listener;
use anyhow::{bail, Error};
use crossbeam::channel::{unbounded as channel, Receiver};
use log::info;
use mux::Mux;
use promise::*;
use std::rc::Rc;
pub struct MuxServerFrontEnd {
rx: Receiver<SpawnFunc>,
}
impl MuxServerFrontEnd {
#[cfg_attr(feature = "cargo-clippy", allow(clippy::new_ret_no_self))]
fn new(start_listener: bool) -> Result<Rc<dyn FrontEnd>, Error> {
let (tx, rx) = channel();
let tx_main = tx.clone();
let tx_low = tx.clone();
let queue_func = move |f: SpawnFunc| {
tx_main.send(f).ok();
};
let queue_func_low = move |f: SpawnFunc| {
tx_low.send(f).ok();
};
promise::spawn::set_schedulers(
Box::new(move |task| queue_func(Box::new(move || task.run()))),
Box::new(move |task| queue_func_low(Box::new(move || task.run()))),
);
if start_listener {
spawn_listener()?;
}
Ok(Rc::new(Self { rx }))
}
pub fn try_new() -> Result<Rc<dyn FrontEnd>, Error> {
Self::new(true)
}
pub fn new_null() -> Result<Rc<dyn FrontEnd>, Error> {
Self::new(false)
}
}
impl FrontEnd for MuxServerFrontEnd {
fn run_forever(&self) -> Result<(), Error> {
loop {
match self.rx.recv() {
Ok(func) => func(),
Err(err) => bail!("while waiting for events: {:?}", err),
}
if Mux::get().unwrap().is_empty() && mux::activity::Activity::count() == 0 {
info!("No more tabs; all done!");
return Ok(());
}
}
}
}

View File

@ -1,7 +1,6 @@
// Don't create a new standard console window when launched from the windows GUI.
#![windows_subsystem = "windows"]
use crate::server::listener::umask;
use anyhow::{anyhow, bail};
use config::{wezterm_version, SshParameters};
use mux::domain::{Domain, LocalDomain};
@ -574,8 +573,7 @@ fn run_terminal_gui(config: config::ConfigHandle, opts: StartCommand) -> anyhow:
let front_end_selection = opts.front_end.unwrap_or(config.front_end);
let gui = crate::frontend::try_new(front_end_selection)?;
let activity = Activity::new();
let do_auto_connect =
front_end_selection != FrontEndSelection::MuxServer && !opts.no_auto_connect;
let do_auto_connect = !opts.no_auto_connect;
promise::spawn::spawn(async move {
if let Err(err) = async_run_terminal_gui(cmd, do_auto_connect).await {
@ -838,8 +836,6 @@ fn run() -> anyhow::Result<()> {
SubCommand::ImageCat(cmd) => cmd.run(),
SubCommand::Cli(cli) => {
// Start a front end so that the futures executor is running
let front_end = crate::frontend::try_new(FrontEndSelection::Null)?;
let initial = true;
let mut ui = crate::connui::ConnectionUI::new_headless();
let client = Client::new_default_unix_domain(initial, &mut ui)?;
@ -971,7 +967,6 @@ fn run() -> anyhow::Result<()> {
let stdin = std::io::stdin();
consume_stream_then_exit_process(stdin.lock(), stream);
});
front_end.run_forever()?;
}
CliSubCommand::TlsCreds => {
let creds = block_on(client.get_tls_creds())?;

View File

@ -1,34 +0,0 @@
use anyhow::{anyhow, bail, Context, Error};
use config::{configuration, TlsDomainServer};
use log::error;
use promise::spawn::spawn_into_main_thread;
use std::net::TcpListener;
use std::path::Path;
use std::sync::Arc;
use std::thread;
mod clientsession;
mod local;
mod ossl;
mod pki;
mod sessionhandler;
pub mod umask;
lazy_static::lazy_static! {
static ref PKI: pki::Pki = pki::Pki::init().expect("failed to initialize PKI");
}
pub fn spawn_listener() -> anyhow::Result<()> {
let config = configuration();
for unix_dom in &config.unix_domains {
let mut listener = local::LocalListener::with_domain(unix_dom)?;
thread::spawn(move || {
listener.run();
});
}
for tls_server in &config.tls_servers {
ossl::spawn_tls_listener(tls_server)?;
}
Ok(())
}

View File

@ -1,10 +1,9 @@
#[cfg(unix)]
use std::os::unix::net::{UnixListener, UnixStream};
use std::os::unix::net::UnixStream;
#[cfg(windows)]
use uds_windows::{UnixListener, UnixStream};
use uds_windows::UnixStream;
pub mod client;
pub mod domain;
pub mod listener;
pub mod pollable;
pub mod tab;

10
umask/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "umask"
version = "0.1.0"
authors = ["Wez Furlong <wez@wezfurlong.org>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
libc = "0.2"

View File

@ -0,0 +1,35 @@
[package]
name = "wezterm-mux-server"
version = "0.1.0"
authors = ["Wez Furlong <wez@wezfurlong.org>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0"
codec = { path = "../codec" }
config = { path = "../config" }
crossbeam = "0.7"
filedescriptor = { version="0.7", path = "../filedescriptor" }
hostname = "0.3"
log = "0.4"
mux = { path = "../mux" }
openssl = "0.10"
portable-pty = { path = "../pty", features = ["serde_support"]}
pretty_env_logger = "0.4"
promise = { path = "../promise" }
rangeset = { path = "../rangeset" }
rcgen = "0.7"
structopt = "0.3"
umask = { path = "../umask" }
url = "2"
wezterm-term = { path = "../term", features=["use_serde"] }
[target.'cfg(unix)'.dependencies]
daemonize = { git = "https://github.com/wez/daemonize" }
[features]
default = ["vendor_openssl"]
# FIXME: find a way to magically disable vendor_openssl only on linux!
vendor_openssl = ["openssl/vendored"]

View File

@ -1,5 +1,5 @@
use crate::server::listener::sessionhandler::SessionHandler;
use crate::server::pollable::*;
use crate::pollable::*;
use crate::sessionhandler::SessionHandler;
use anyhow::{bail, Context, Error};
use codec::*;
use crossbeam::channel::TryRecvError;

View File

@ -1,5 +1,5 @@
use crate::server::listener::clientsession;
use crate::server::UnixListener;
use crate::clientsession;
use crate::UnixListener;
use anyhow::{anyhow, Context as _};
use config::{create_user_owned_dirs, UnixDomain};
use promise::spawn::spawn_into_main_thread;

View File

@ -0,0 +1,218 @@
use anyhow::{anyhow, bail, Context, Error};
use config::{configuration, TlsDomainServer};
use crossbeam::channel::unbounded as channel;
use log::error;
use mux::activity::Activity;
use mux::domain::{Domain, LocalDomain};
use mux::Mux;
use portable_pty::cmdbuilder::CommandBuilder;
use promise::spawn::spawn_into_main_thread;
use promise::*;
use std::ffi::OsString;
use std::net::TcpListener;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use std::thread;
use structopt::*;
#[cfg(unix)]
use std::os::unix::net::{UnixListener, UnixStream};
#[cfg(windows)]
use uds_windows::{UnixListener, UnixStream};
#[derive(Debug, StructOpt)]
#[structopt(
about = "Wez's Terminal Emulator\nhttp://github.com/wez/wezterm",
global_setting = structopt::clap::AppSettings::ColoredHelp,
version = config::wezterm_version()
)]
struct Opt {
/// Skip loading wezterm.lua
#[structopt(short = "n")]
skip_config: bool,
/// Detach from the foreground and become a background process
#[structopt(long = "daemonize")]
daemonize: bool,
/// Specify the current working directory for the initially
/// spawned program
#[structopt(long = "cwd", parse(from_os_str))]
cwd: Option<OsString>,
/// Instead of executing your shell, run PROG.
/// For example: `wezterm start -- bash -l` will spawn bash
/// as if it were a login shell.
#[structopt(parse(from_os_str))]
prog: Vec<OsString>,
}
fn main() -> anyhow::Result<()> {
pretty_env_logger::init_timed();
//stats::Stats::init()?;
let _saver = umask::UmaskSaver::new();
let opts = Opt::from_args();
if !opts.skip_config {
config::reload();
}
let config = config::configuration();
#[cfg(unix)]
{
if opts.daemonize {
let stdout = config.daemon_options.open_stdout()?;
let stderr = config.daemon_options.open_stderr()?;
let mut daemonize = daemonize::Daemonize::new()
.stdout(stdout)
.stderr(stderr)
.working_directory(config::HOME_DIR.clone());
if !config::running_under_wsl() {
// pid file locking is only partly function when running under
// WSL 1; it is possible for the pid file to exist after a reboot
// and for attempts to open and lock it to fail when there are no
// other processes that might possibly hold a lock on it.
// So, we only use a pid file when not under WSL.
daemonize = daemonize.pid_file(config.daemon_options.pid_file());
}
if let Err(err) = daemonize.start() {
use daemonize::DaemonizeError;
match err {
DaemonizeError::OpenPidfile
| DaemonizeError::LockPidfile(_)
| DaemonizeError::ChownPidfile(_)
| DaemonizeError::WritePid => {
bail!("{} {}", err, config.daemon_options.pid_file().display());
}
DaemonizeError::ChangeDirectory => {
bail!("{} {}", err, config::HOME_DIR.display());
}
_ => return Err(err.into()),
}
}
// Remove some environment variables that aren't super helpful or
// that are potentially misleading when we're starting up the
// server.
// We may potentially want to look into starting/registering
// a session of some kind here as well in the future.
for name in &[
"OLDPWD",
"PWD",
"SHLVL",
"SSH_AUTH_SOCK",
"SSH_CLIENT",
"SSH_CONNECTION",
"_",
] {
std::env::remove_var(name);
}
}
}
let need_builder = !opts.prog.is_empty() || opts.cwd.is_some();
let cmd = if need_builder {
let mut builder = if opts.prog.is_empty() {
CommandBuilder::new_default_prog()
} else {
CommandBuilder::from_argv(opts.prog)
};
if let Some(cwd) = opts.cwd {
builder.cwd(cwd);
}
Some(builder)
} else {
None
};
let domain: Arc<dyn Domain> = Arc::new(LocalDomain::new("local")?);
let mux = Rc::new(mux::Mux::new(Some(domain.clone())));
Mux::set_mux(&mux);
let (tx, rx) = channel();
let tx_main = tx.clone();
let tx_low = tx.clone();
let queue_func = move |f: SpawnFunc| {
tx_main.send(f).ok();
};
let queue_func_low = move |f: SpawnFunc| {
tx_low.send(f).ok();
};
promise::spawn::set_schedulers(
Box::new(move |task| queue_func(Box::new(move || task.run()))),
Box::new(move |task| queue_func_low(Box::new(move || task.run()))),
);
spawn_listener()?;
let activity = Activity::new();
promise::spawn::spawn(async move {
if let Err(err) = async_run(cmd).await {
terminate_with_error(err);
}
drop(activity);
});
loop {
match rx.recv() {
Ok(func) => func(),
Err(err) => bail!("while waiting for events: {:?}", err),
}
if Mux::get().unwrap().is_empty() && mux::activity::Activity::count() == 0 {
log::info!("No more tabs; all done!");
return Ok(());
}
}
}
async fn async_run(cmd: Option<CommandBuilder>) -> anyhow::Result<()> {
let mux = Mux::get().unwrap();
let domain = mux.default_domain();
domain.attach().await?;
let config = config::configuration();
let window_id = mux.new_empty_window();
let _tab = mux
.default_domain()
.spawn(config.initial_size(), cmd, None, *window_id)
.await?;
Ok(())
}
fn terminate_with_error(err: anyhow::Error) -> ! {
log::error!("{:#}; terminating", err);
std::process::exit(1);
}
mod clientsession;
mod local;
mod ossl;
mod pki;
mod pollable;
mod sessionhandler;
lazy_static::lazy_static! {
static ref PKI: pki::Pki = pki::Pki::init().expect("failed to initialize PKI");
}
pub fn spawn_listener() -> anyhow::Result<()> {
let config = configuration();
for unix_dom in &config.unix_domains {
let mut listener = local::LocalListener::with_domain(unix_dom)?;
thread::spawn(move || {
listener.run();
});
}
for tls_server in &config.tls_servers {
ossl::spawn_tls_listener(tls_server)?;
}
Ok(())
}

View File

@ -0,0 +1,131 @@
use crate::UnixStream;
use anyhow::Error;
use crossbeam::channel::{unbounded as channel, Receiver, Sender, TryRecvError};
use filedescriptor::*;
use std::cell::RefCell;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::{Arc, Mutex};
pub trait ReadAndWrite: std::io::Read + std::io::Write + Send + AsPollFd {
fn set_non_blocking(&self, non_blocking: bool) -> anyhow::Result<()>;
fn has_read_buffered(&self) -> bool;
}
impl ReadAndWrite for UnixStream {
fn set_non_blocking(&self, non_blocking: bool) -> anyhow::Result<()> {
self.set_nonblocking(non_blocking)?;
Ok(())
}
fn has_read_buffered(&self) -> bool {
false
}
}
impl ReadAndWrite for openssl::ssl::SslStream<std::net::TcpStream> {
fn set_non_blocking(&self, non_blocking: bool) -> anyhow::Result<()> {
self.get_ref().set_nonblocking(non_blocking)?;
Ok(())
}
fn has_read_buffered(&self) -> bool {
self.ssl().pending() != 0
}
}
pub struct PollableSender<T> {
sender: Sender<T>,
write: Arc<Mutex<FileDescriptor>>,
}
impl<T: Send + Sync + 'static> PollableSender<T> {
pub fn send(&self, item: T) -> anyhow::Result<()> {
// Attempt to write to the pipe; if it fails due to
// being full, that's fine: it means that the other end
// is going to be signalled already so they won't miss
// anything if our write doesn't happen.
self.write.lock().unwrap().write_all(b"x").ok();
self.sender.send(item).map_err(Error::msg)?;
Ok(())
}
}
impl<T> Clone for PollableSender<T> {
fn clone(&self) -> Self {
Self {
sender: self.sender.clone(),
write: self.write.clone(),
}
}
}
pub struct PollableReceiver<T> {
receiver: Receiver<T>,
read: RefCell<FileDescriptor>,
}
impl<T> PollableReceiver<T> {
pub fn try_recv(&self) -> Result<T, TryRecvError> {
// try to drain the pipe.
// We do this regardless of whether we popped an item
// so that we avoid being in a perpetually signalled state.
let mut byte = [0u8; 64];
self.read.borrow_mut().read(&mut byte).ok();
Ok(self.receiver.try_recv()?)
}
}
impl<T> AsPollFd for PollableReceiver<T> {
fn as_poll_fd(&self) -> pollfd {
self.read.borrow().as_socket_descriptor().as_poll_fd()
}
}
/// A channel that can be polled together with a socket.
/// This uses the self-pipe trick but with a unix domain
/// socketpair.
/// In theory this should also work on windows, but will require
/// windows 10 w/unix domain socket support.
pub fn pollable_channel<T>() -> anyhow::Result<(PollableSender<T>, PollableReceiver<T>)> {
let (sender, receiver) = channel();
let (mut write, mut read) = socketpair()?;
write.set_non_blocking(true)?;
read.set_non_blocking(true)?;
Ok((
PollableSender {
sender,
write: Arc::new(Mutex::new(FileDescriptor::new(write))),
},
PollableReceiver {
receiver,
read: RefCell::new(FileDescriptor::new(read)),
},
))
}
pub trait AsPollFd {
fn as_poll_fd(&self) -> pollfd;
}
impl AsPollFd for SocketDescriptor {
fn as_poll_fd(&self) -> pollfd {
pollfd {
fd: *self,
events: POLLIN,
revents: 0,
}
}
}
impl AsPollFd for openssl::ssl::SslStream<TcpStream> {
fn as_poll_fd(&self) -> pollfd {
self.get_ref().as_socket_descriptor().as_poll_fd()
}
}
impl AsPollFd for UnixStream {
fn as_poll_fd(&self) -> pollfd {
self.as_socket_descriptor().as_poll_fd()
}
}

View File

@ -1,5 +1,5 @@
use crate::server::listener::PKI;
use crate::server::pollable::*;
use crate::pollable::*;
use crate::PKI;
use anyhow::anyhow;
use codec::*;
use config::keyassignment::SpawnTabDomain;
@ -447,7 +447,7 @@ impl SessionHandler {
Pdu::GetCodecVersion(_) => {
send_response(Ok(Pdu::GetCodecVersionResponse(GetCodecVersionResponse {
codec_vers: CODEC_VERSION,
version_string: crate::wezterm_version().to_owned(),
version_string: config::wezterm_version().to_owned(),
})))
}