Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use futures_util::FutureExt as _;
|
||||
use itertools::Itertools as _;
|
||||
use warpui::{r#async::executor::BackgroundTask, AppContext, SingletonEntity};
|
||||
use zbus::{interface, proxy, zvariant};
|
||||
|
||||
use crate::channel::ChannelState;
|
||||
use crate::report_if_error;
|
||||
|
||||
/// Initializes application services.
|
||||
pub fn init(ctx: &mut AppContext) {
|
||||
ctx.add_singleton_model(DBusServiceHost::new);
|
||||
}
|
||||
|
||||
/// Tears down application services.
|
||||
pub fn teardown(ctx: &mut AppContext) {
|
||||
DBusServiceHost::handle(ctx).update(ctx, |service_host, _| {
|
||||
service_host.terminate();
|
||||
});
|
||||
}
|
||||
|
||||
/// Attempts to forward startup arguments to an existing instance of the
|
||||
/// application.
|
||||
///
|
||||
/// Returns Ok if an existing instance exists and was reachable.
|
||||
#[cfg(feature = "release_bundle")]
|
||||
pub fn pass_startup_args_to_existing_instance(
|
||||
args: &warp_cli::AppArgs,
|
||||
) -> Result<(), StartupArgsForwardingError> {
|
||||
if args.finish_update {
|
||||
return Err(StartupArgsForwardingError::IgnoredAfterAutoUpdate);
|
||||
}
|
||||
|
||||
warpui::r#async::block_on(async {
|
||||
let conn = zbus::Connection::session().await?;
|
||||
let proxy = ExistingApplicationProxy::builder(&conn)
|
||||
.destination(DBusServiceHost::well_known_name())?
|
||||
.path(DBusServiceHost::application_service_path())?
|
||||
.build()
|
||||
.await?;
|
||||
let mut open_new_url;
|
||||
let mut url_refs = args.urls.iter().map(AsRef::as_ref).collect_vec();
|
||||
// If there are no URLs on the command line, send one to open a new
|
||||
// window using the same current working directory as this process.
|
||||
if url_refs.is_empty() {
|
||||
open_new_url = format!("{}://action/new_window", ChannelState::url_scheme());
|
||||
if let Ok(current_dir) = std::env::current_dir() {
|
||||
open_new_url.push_str(&format!("?path={}", current_dir.display()));
|
||||
}
|
||||
url_refs.push(&open_new_url);
|
||||
}
|
||||
proxy.open(&url_refs, HashMap::new()).await?;
|
||||
|
||||
// Make sure we close the connection and clean up resources, to avoid
|
||||
// leaving behind file descriptors that will interfere with the terminal
|
||||
// server spawn process.
|
||||
let _ = conn.close().await;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[cfg(feature = "release_bundle")]
|
||||
pub enum StartupArgsForwardingError {
|
||||
/// There's no instance of Warp already running.
|
||||
#[error("no existing instance found to forward args to")]
|
||||
NoExistingInstance,
|
||||
/// This instance was launched after an auto-update and should not forward
|
||||
/// arguments to the old (terminating) instance.
|
||||
#[error("should not forward args after an auto-update")]
|
||||
IgnoredAfterAutoUpdate,
|
||||
/// An unknown D-Bus error occurred.
|
||||
#[error("unknown dbus error")]
|
||||
Unknown(zbus::Error),
|
||||
}
|
||||
|
||||
#[cfg(feature = "release_bundle")]
|
||||
impl From<zbus::fdo::Error> for StartupArgsForwardingError {
|
||||
fn from(value: zbus::fdo::Error) -> Self {
|
||||
// While ServiceUnknown usually means that D-Bus doesn't know how to
|
||||
// _launch_ something to handle your message, in our case, we're not
|
||||
// registering a service, so this really means that Warp is not already
|
||||
// running.
|
||||
if matches!(value, zbus::fdo::Error::ServiceUnknown(_)) {
|
||||
StartupArgsForwardingError::NoExistingInstance
|
||||
} else {
|
||||
StartupArgsForwardingError::Unknown(zbus::Error::FDO(Box::new(value)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "release_bundle")]
|
||||
impl From<zbus::Error> for StartupArgsForwardingError {
|
||||
fn from(value: zbus::Error) -> Self {
|
||||
match value {
|
||||
zbus::Error::FDO(err) => (*err).into(),
|
||||
err => StartupArgsForwardingError::Unknown(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ApplicationServiceEvent {
|
||||
Open { uris: Vec<String> },
|
||||
}
|
||||
|
||||
/// A structure providing an implementation of the org.freedesktop.Application
|
||||
/// D-Bus service.
|
||||
struct ApplicationService {
|
||||
tx: async_channel::Sender<ApplicationServiceEvent>,
|
||||
}
|
||||
|
||||
/// An implementation of the org.freedesktop.Application D-Bus service.
|
||||
///
|
||||
/// See: https://specifications.freedesktop.org/desktop-entry-spec/1.5/ar01s08.html
|
||||
#[interface(name = "org.freedesktop.Application")]
|
||||
impl ApplicationService {
|
||||
/// Called when the application is started without any files to open.
|
||||
async fn activate(
|
||||
&self,
|
||||
_platform_data: HashMap<String, zvariant::Value<'_>>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
// not yet implemented
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Called when desktop actions are activated.
|
||||
///
|
||||
/// See: https://specifications.freedesktop.org/desktop-entry-spec/1.5/ar01s11.html
|
||||
async fn activate_action(
|
||||
&self,
|
||||
_action_name: String,
|
||||
_parameter: Vec<zvariant::Value<'_>>,
|
||||
_platform_data: HashMap<String, zvariant::Value<'_>>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
// not yet implemented
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Called when the application is started with files.
|
||||
async fn open(
|
||||
&self,
|
||||
uris: Vec<String>,
|
||||
_platform_data: HashMap<String, zvariant::Value<'_>>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let _ = self.tx.send(ApplicationServiceEvent::Open { uris }).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// A D-Bus client for connecting to an already-running instance of Warp and
|
||||
// invoking org.freedesktop.Application IPC methods.
|
||||
#[proxy(
|
||||
interface = "org.freedesktop.Application",
|
||||
default_service = "dev.warp.WarpLocal",
|
||||
default_path = "/dev/warp/WarpLocal",
|
||||
gen_blocking = false
|
||||
)]
|
||||
trait ExistingApplication {
|
||||
fn activate(&self, platform_data: HashMap<&str, zvariant::Value<'_>>) -> zbus::fdo::Result<()>;
|
||||
|
||||
fn activate_action(
|
||||
&self,
|
||||
action_name: &str,
|
||||
parameter: &[zvariant::Value<'_>],
|
||||
platform_data: HashMap<&str, zvariant::Value<'_>>,
|
||||
) -> zbus::fdo::Result<()>;
|
||||
|
||||
fn open(
|
||||
&self,
|
||||
uris: &[&str],
|
||||
platform_data: HashMap<&str, zvariant::Value<'_>>,
|
||||
) -> zbus::fdo::Result<()>;
|
||||
}
|
||||
|
||||
/// A singleton model that is responsible for hosting all D-Bus services
|
||||
/// exposed by the application.
|
||||
struct DBusServiceHost {
|
||||
server_task: Option<BackgroundTask>,
|
||||
}
|
||||
|
||||
impl DBusServiceHost {
|
||||
fn new(ctx: &mut warpui::ModelContext<Self>) -> Self {
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
|
||||
// Spawn a background task for the D-Bus server.
|
||||
let server_task = ctx.background_executor().spawn(
|
||||
async {
|
||||
let conn = zbus::connection::Builder::session()?
|
||||
.name(Self::well_known_name())?
|
||||
.serve_at(Self::application_service_path(), ApplicationService { tx })?
|
||||
// Instead of having zbus spawn a thread to poll for new
|
||||
// messages, we'll poll on our own executor.
|
||||
.internal_executor(false)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
loop {
|
||||
conn.executor().tick().await;
|
||||
}
|
||||
}
|
||||
.map(|result: anyhow::Result<()>| {
|
||||
if let Err(err) = result {
|
||||
log::error!(
|
||||
"Failed to initialize org.freedesktop.Application D-Bus service: {err:#}"
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Process any events that we receive over D-Bus.
|
||||
ctx.spawn_stream_local(rx, |_, event, ctx| {
|
||||
match event {
|
||||
ApplicationServiceEvent::Open { uris } => {
|
||||
for uri in uris {
|
||||
match url::Url::parse(&uri) {
|
||||
Ok(uri) => crate::uri::handle_incoming_uri(&uri, ctx),
|
||||
Err(err) => log::warn!("Failed to parse URI when handling org.freedesktop.Application/open: {err:#}"),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}, |_, _| {});
|
||||
|
||||
Self {
|
||||
server_task: Some(server_task),
|
||||
}
|
||||
}
|
||||
|
||||
fn terminate(&mut self) {
|
||||
if let Some(server_task) = self.server_task.take() {
|
||||
server_task.abort();
|
||||
// Wait until we've torn down the dbus service.
|
||||
report_if_error!(warpui::r#async::block_on(server_task));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the D-Bus well-known name that should be used.
|
||||
fn well_known_name() -> String {
|
||||
ChannelState::app_id().to_string()
|
||||
}
|
||||
|
||||
/// Returns the path under which the org.freedesktop.Application interface
|
||||
/// will be hosted.
|
||||
fn application_service_path() -> String {
|
||||
format!("/{}", Self::well_known_name().split('.').join("/"))
|
||||
}
|
||||
}
|
||||
|
||||
impl warpui::Entity for DBusServiceHost {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl SingletonEntity for DBusServiceHost {}
|
||||
@@ -0,0 +1,30 @@
|
||||
#[allow(deprecated)]
|
||||
use cocoa::base::id;
|
||||
use warpui::platform::mac::make_nsstring;
|
||||
|
||||
use crate::channel::ChannelState;
|
||||
|
||||
extern "C" {
|
||||
/// ObjC function to create and register the NSServices provider for the
|
||||
/// application.
|
||||
fn warp_register_services_provider();
|
||||
}
|
||||
|
||||
/// Initializes application services.
|
||||
pub fn init() {
|
||||
unsafe {
|
||||
warp_register_services_provider();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an NSString containing the custom URL scheme that this build of the
|
||||
/// application will respond to.
|
||||
///
|
||||
/// Called synchronously from the NSServices dispatch path in
|
||||
/// `services.m::forFilesFromPasteboard:performAction:`, which wraps the body in
|
||||
/// an `@autoreleasepool` block. That ambient pool owns the returned NSString.
|
||||
#[allow(deprecated)]
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_services_provider_custom_url_scheme() -> id {
|
||||
make_nsstring(ChannelState::url_scheme())
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! Functionality relating to services that the application provides
|
||||
//! to the host system.
|
||||
//!
|
||||
//! For example, on macOS, this module sets up integrations with
|
||||
//! Finder such that the user can open a new Warp tab or window
|
||||
//! in a given directory.
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
|
||||
use warpui::AppContext;
|
||||
|
||||
pub fn init(_ctx: &mut AppContext) {
|
||||
log::info!("Initializing app services");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
linux::init(_ctx);
|
||||
#[cfg(target_os = "macos")]
|
||||
mac::init();
|
||||
#[cfg(windows)]
|
||||
windows::init(_ctx);
|
||||
}
|
||||
|
||||
pub fn teardown(_ctx: &mut AppContext) {
|
||||
log::info!("Tearing down app services...");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
linux::teardown(_ctx);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use registry::register_uri_handler;
|
||||
use warpui::AppContext;
|
||||
#[cfg(feature = "release_bundle")]
|
||||
use {
|
||||
service_impl::forward_uri_to_sole_running_instance,
|
||||
single_instance_manager::SingleInstanceManager, thiserror::Error, url::Url,
|
||||
warp_core::channel::ChannelState,
|
||||
};
|
||||
|
||||
mod registry;
|
||||
#[cfg(feature = "release_bundle")]
|
||||
mod service_impl;
|
||||
#[cfg(feature = "release_bundle")]
|
||||
mod single_instance_manager;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[cfg(feature = "release_bundle")]
|
||||
pub enum StartupArgsForwardingError {
|
||||
#[error("should not forward arguments after an auto-update")]
|
||||
IgnoredAfterAutoUpdate,
|
||||
#[error("there is no other instance of Warp")]
|
||||
NoExistingInstance,
|
||||
#[error("failed to construct url")]
|
||||
CouldNotCreateUrl(#[from] url::ParseError),
|
||||
#[error("IPC Client failed to send message")]
|
||||
IpcError(#[from] ipc::ClientError),
|
||||
#[error("Win32 error")]
|
||||
WindowsError(#[from] windows::core::Error),
|
||||
}
|
||||
|
||||
#[cfg(feature = "release_bundle")]
|
||||
pub fn pass_startup_args_to_existing_instance(
|
||||
args: &warp_cli::AppArgs,
|
||||
) -> Result<(), StartupArgsForwardingError> {
|
||||
if args.finish_update {
|
||||
return Err(StartupArgsForwardingError::IgnoredAfterAutoUpdate);
|
||||
}
|
||||
if SingleInstanceManager::is_sole_running_instance()? {
|
||||
return Err(StartupArgsForwardingError::NoExistingInstance);
|
||||
}
|
||||
|
||||
warpui::r#async::block_on(async {
|
||||
if args.urls.is_empty() {
|
||||
// If there are no URLs on the command line, send one to open a new
|
||||
// window using the same current working directory as this process.
|
||||
let mut open_new_url = format!("{}://action/new_window", ChannelState::url_scheme());
|
||||
if let Ok(current_dir) = std::env::current_dir() {
|
||||
match current_dir.into_os_string().into_string() {
|
||||
Ok(current_dir) => open_new_url.push_str(&format!("?path={}", current_dir)),
|
||||
Err(os_string) => {
|
||||
log::error!("Failed to convert OsString {os_string:?} to ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let url = Url::parse(&open_new_url)?;
|
||||
forward_uri_to_sole_running_instance(vec![url]).await?
|
||||
} else {
|
||||
forward_uri_to_sole_running_instance(args.urls.clone()).await?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn init(_ctx: &mut AppContext) {
|
||||
#[cfg(feature = "release_bundle")]
|
||||
_ctx.add_singleton_model(SingleInstanceManager::new);
|
||||
register_uri_handler();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::ffi::OsString;
|
||||
|
||||
use warp_core::channel::ChannelState;
|
||||
use windows_registry::{CURRENT_USER, HSTRING};
|
||||
|
||||
pub(super) fn register_uri_handler() {
|
||||
// To change the settings for the user, changes must be made under
|
||||
// HKEY_CURRENT_USER\Software\Classes instead of under HKEY_CLASSES_ROOT since only an
|
||||
// administrator can modify it. It gets merged into HKEY_CLASSES_ROOT later.
|
||||
let Ok(classes_key) = CURRENT_USER.open("Software\\Classes") else {
|
||||
log::error!("Failed to get current_user\\software\\classes");
|
||||
return;
|
||||
};
|
||||
|
||||
// The Windows Registry entry for Warp (assuming the channel is WarpLocal):
|
||||
// warplocal
|
||||
// (Default) = "WarpLocal"
|
||||
// URL Protocol = ""
|
||||
// DefaultIcon
|
||||
// (Default) = "{path_to_channel_icon},0" TODO(CORE-2860): Add icon file path here.
|
||||
// shell
|
||||
// open
|
||||
// command
|
||||
// (Default) = "{path_to_executable}" "%0"
|
||||
let uri_scheme = ChannelState::url_scheme();
|
||||
match classes_key.create(uri_scheme) {
|
||||
Ok(parent_key) => {
|
||||
// The empty string represents the "(Default)" value for a registry key.
|
||||
if let Err(err) = parent_key.set_string("", ChannelState::app_id().application_name()) {
|
||||
log::error!("Could not set URI Scheme display name: {err:?}");
|
||||
return;
|
||||
}
|
||||
if let Err(err) = parent_key.set_string("URL Protocol", "") {
|
||||
log::error!("Could not set URI Scheme URL Protocol Key: {err:?}");
|
||||
return;
|
||||
};
|
||||
|
||||
// TODO(CORE-2861): Add the `DefaultIcon` Default value here with the file path to
|
||||
// Warp's icon once we figure out distribution on Windows.
|
||||
|
||||
let command_key = match parent_key.create("shell\\open\\command") {
|
||||
Ok(command_key) => command_key,
|
||||
Err(err) => {
|
||||
log::error!("Could not create shell\\open\\command key: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let command = match std::env::current_exe() {
|
||||
Ok(path) => {
|
||||
let mut command = OsString::new();
|
||||
command.push("\"");
|
||||
command.push(path.as_os_str());
|
||||
command.push("\" \"%0\"");
|
||||
HSTRING::from(command.as_os_str())
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Could not get path to current executable for registering URI scheme: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// The empty string represents the "(Default)" value for a registry key.
|
||||
if let Err(err) = command_key.set_hstring("", &command) {
|
||||
log::error!("Could not set shell command path for URI Scheme: {err:?}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to create URI Scheme registry entry: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_channel::Sender;
|
||||
use async_trait::async_trait;
|
||||
use ipc::{Client, ConnectionAddress};
|
||||
use url::Url;
|
||||
use warpui::r#async::executor::Background;
|
||||
|
||||
use super::single_instance_manager::uri_named_pipe_name;
|
||||
|
||||
/// IPC Service to respond to URIs sent to the active Warp instance.
|
||||
pub(super) struct UriService {}
|
||||
|
||||
impl ipc::Service for UriService {
|
||||
type Request = Vec<Url>;
|
||||
type Response = ();
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct UriServiceImpl {
|
||||
tx: Sender<Vec<Url>>,
|
||||
}
|
||||
|
||||
impl UriServiceImpl {
|
||||
pub(super) fn new(tx: Sender<Vec<Url>>) -> Self {
|
||||
Self { tx }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ipc::ServiceImpl for UriServiceImpl {
|
||||
type Service = UriService;
|
||||
|
||||
async fn handle_request(&self, request: Vec<Url>) -> () {
|
||||
log::info!("Uri Service received request: {request:?}");
|
||||
if let Err(send_error) = self.tx.send(request).await {
|
||||
log::error!("Error sending urls to local stream: {send_error:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Forwards the given URLs to the main running instance of Warp.
|
||||
pub(super) async fn forward_uri_to_sole_running_instance(
|
||||
urls: Vec<Url>,
|
||||
) -> Result<(), ipc::ClientError> {
|
||||
// We need to construct a new background executor because this function is
|
||||
// run before we have a `AppContext`. We explicitly create it with
|
||||
// a single backing thread, as we don't need an entire pool of threads.
|
||||
let background_executor = Arc::new(Background::new(1, |_| "forward-uris".to_owned()));
|
||||
let client = Client::connect(
|
||||
ConnectionAddress::from(uri_named_pipe_name()),
|
||||
background_executor,
|
||||
)
|
||||
.await?;
|
||||
let uri_service_caller = ipc::service_caller::<UriService>(Arc::new(client));
|
||||
let _ = uri_service_caller.call(urls).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use ipc::ServerBuilder;
|
||||
use parking_lot::Mutex;
|
||||
use warp_core::channel::ChannelState;
|
||||
use warpui::{Entity, ModelContext, SingletonEntity};
|
||||
|
||||
use windows::core::Error;
|
||||
use windows::Win32::Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, HANDLE};
|
||||
use windows::Win32::System::Threading::CreateMutexW;
|
||||
|
||||
use super::service_impl::UriServiceImpl;
|
||||
|
||||
/// RAII wrapper around a Windows mutex HANDLE that closes it on drop.
|
||||
struct MutexHandle(HANDLE);
|
||||
|
||||
// SAFETY: Windows kernel mutexes are valid to use from any thread. For example it says here:
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createmutexw#remarks
|
||||
// > "Any thread of the calling process can specify the mutex-object handle in a call to one of the
|
||||
// wait functions"
|
||||
// The [`HANDLE`] is not Send or Sync b/c it's a common type used to point to a variety of Windows
|
||||
// kernel objects, many of which are not safe to access from other threads.
|
||||
unsafe impl Send for MutexHandle {}
|
||||
unsafe impl Sync for MutexHandle {}
|
||||
|
||||
impl Drop for MutexHandle {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = CloseHandle(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The single-instance mutex handle. Lives for the process lifetime.
|
||||
///
|
||||
/// It's a complex type. Breaking it down:
|
||||
/// * LazyLock - This type lets us go from un-initialized to initialized without `mut` and _not_
|
||||
/// vice-versa.
|
||||
/// * Mutex - Gives us interior mutability. Unlike `RefCell` it can be used in statics since it is
|
||||
/// Sync. We don't actually need to access it on other threads though.
|
||||
/// * Result - CreateMutexW might fail for reasons other than another process holding the lock. In
|
||||
/// those cases, we store the error type.
|
||||
/// * Option - `Some` if we are the sole instance, `None` if another instance holds the lock.
|
||||
static SOLE_INSTANCE_MUTEX: LazyLock<Mutex<Result<Option<MutexHandle>, Error>>> =
|
||||
LazyLock::new(|| Mutex::new(try_create_mutex()));
|
||||
|
||||
pub(super) fn uri_named_pipe_name() -> String {
|
||||
format!("Warp{:?}_URI_CHANNEL", ChannelState::channel())
|
||||
}
|
||||
|
||||
fn try_create_mutex() -> Result<Option<MutexHandle>, Error> {
|
||||
// Scope this lock to the specific user session.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/termserv/kernel-object-namespaces
|
||||
// > "client processes can use the "Local\" prefix to explicitly create an object in their
|
||||
// session namespace"
|
||||
//
|
||||
// NOTE: This lock name must stay in sync with `AppMutexName` in
|
||||
// `script/windows/windows-installer.iss`, which the installer uses to detect whether Warp is
|
||||
// running.
|
||||
let name = format!("Local\\Warp{:?}_SingleInstance", ChannelState::channel())
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect::<Vec<u16>>();
|
||||
let handle = unsafe { CreateMutexW(None, true, windows::core::PCWSTR(name.as_ptr())) };
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createmutexw#return-value
|
||||
let already_exists = unsafe { GetLastError() } == ERROR_ALREADY_EXISTS;
|
||||
handle
|
||||
.inspect_err(|err| {
|
||||
log::error!("Failed to create single-instance mutex: {err:#}");
|
||||
})
|
||||
.map(|handle| {
|
||||
if already_exists {
|
||||
// Another instance already owns this mutex. Close our duplicate handle.
|
||||
unsafe {
|
||||
let _ = CloseHandle(handle);
|
||||
}
|
||||
None
|
||||
} else {
|
||||
Some(MutexHandle(handle))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A singleton model that is responsible for ensuring there is only one instance of Warp running.
|
||||
/// Uses a Windows named mutex (via `CreateMutexW`) which is a kernel object automatically cleaned
|
||||
/// up by the OS when all handles are closed, including on crash.
|
||||
pub(super) struct SingleInstanceManager {
|
||||
_server: Option<ipc::Server>,
|
||||
}
|
||||
|
||||
impl SingleInstanceManager {
|
||||
/// Attempts to upgrade the current Warp instance to the "main" instance (i.e. the one that
|
||||
/// holds the named mutex). This function enforces that a URI server is created iff the mutex
|
||||
/// is held.
|
||||
pub(super) fn new(ctx: &mut ModelContext<Self>) -> Self {
|
||||
if let Ok(None) | Err(_) = &*SOLE_INSTANCE_MUTEX.lock() {
|
||||
return Self { _server: None };
|
||||
}
|
||||
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
let server = match ServerBuilder::default()
|
||||
.with_fixed_address(uri_named_pipe_name())
|
||||
.with_service(UriServiceImpl::new(tx))
|
||||
.build_and_run(ctx.background_executor())
|
||||
{
|
||||
Ok((server, _)) => {
|
||||
ctx.spawn_stream_local(
|
||||
rx,
|
||||
|_single_instance_manager, event, ctx| {
|
||||
for uri in event {
|
||||
crate::uri::handle_incoming_uri(&uri, ctx);
|
||||
}
|
||||
},
|
||||
|_, _| {},
|
||||
);
|
||||
server
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to initialize UriService Server: {err:#}");
|
||||
// If we failed to create a server, we can't receive URI requests so we drop the
|
||||
// lock.
|
||||
*SOLE_INSTANCE_MUTEX.lock() = Ok(None);
|
||||
return Self { _server: None };
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
_server: Some(server),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether or not this process should be treated as the main instance of Warp.
|
||||
///
|
||||
/// NOTE: If an unexpected error occurs, we return `true` since it's better to open a second
|
||||
/// instance than to fail to create a first instance.
|
||||
pub(super) fn is_sole_running_instance() -> Result<bool, Error> {
|
||||
SOLE_INSTANCE_MUTEX
|
||||
.lock()
|
||||
.as_ref()
|
||||
.map(|handle| handle.is_some())
|
||||
.map_err(Clone::clone)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for SingleInstanceManager {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl SingletonEntity for SingleInstanceManager {}
|
||||
Reference in New Issue
Block a user