Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -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