Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
use crate::terminal::{
|
||||
bootstrap::init_shell_script_for_shell, event_listener::ChannelEventListener,
|
||||
model::ansi::Processor, session_settings::SessionSettings, shell::ShellType,
|
||||
writeable_pty::Message as EventLoopMessage, SizeInfo, TerminalModel,
|
||||
};
|
||||
use async_channel::Receiver;
|
||||
use futures_util::SinkExt;
|
||||
use parking_lot::FairMutex;
|
||||
use serde::Serialize;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use warpui::{Entity, ModelContext, SingletonEntity};
|
||||
use websocket::{Message, Sink, Stream, WebSocket, WebsocketMessage as _};
|
||||
|
||||
const CREATE_SESSION_ENDPOINT: &str = "ws://127.0.0.1:3030/create";
|
||||
|
||||
/// Contains info needed to resize the SSH terminal session. Is serialized and
|
||||
/// sent over the websocket as text.
|
||||
///
|
||||
/// The field names need to be kept the same as the `WindowSizeChange` struct in
|
||||
/// https://github.com/warpdotdev/ssh-proxy-server/blob/main/src/ssh/session.rs.
|
||||
#[derive(Serialize, Debug)]
|
||||
struct WindowSizeChange {
|
||||
width: u32,
|
||||
height: u32,
|
||||
width_px: u32,
|
||||
height_px: u32,
|
||||
}
|
||||
|
||||
pub(super) struct EventLoop {
|
||||
terminal_model: Arc<FairMutex<TerminalModel>>,
|
||||
parser: Processor,
|
||||
event_loop_rx: Receiver<EventLoopMessage>,
|
||||
channel_event_listener: ChannelEventListener,
|
||||
}
|
||||
|
||||
impl EventLoop {
|
||||
/// Starts the [`EventLoop`] by starting a websocket connection with the server and
|
||||
/// bootstrapping the PTY.
|
||||
pub(super) fn start(
|
||||
model: Arc<FairMutex<TerminalModel>>,
|
||||
websocket_receiver: Receiver<EventLoopMessage>,
|
||||
channel_event_listener: ChannelEventListener,
|
||||
size_info: SizeInfo,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let event_loop = Self::new(model, websocket_receiver, channel_event_listener);
|
||||
|
||||
let url = Self::get_new_session_url(size_info);
|
||||
let response = WebSocket::connect(url, None /* protocols */);
|
||||
|
||||
ctx.spawn(response, Self::on_ws_connection);
|
||||
|
||||
event_loop
|
||||
}
|
||||
|
||||
fn new(
|
||||
terminal_model: Arc<FairMutex<TerminalModel>>,
|
||||
websocket_receiver: Receiver<EventLoopMessage>,
|
||||
channel_event_listener: ChannelEventListener,
|
||||
) -> Self {
|
||||
Self {
|
||||
terminal_model,
|
||||
parser: Processor::default(),
|
||||
event_loop_rx: websocket_receiver,
|
||||
channel_event_listener,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_new_session_url(size_info: SizeInfo) -> String {
|
||||
let num_rows = size_info.rows;
|
||||
let num_cols = size_info.columns;
|
||||
|
||||
format!("{CREATE_SESSION_ENDPOINT}?num_rows={num_rows}&num_cols={num_cols}")
|
||||
}
|
||||
|
||||
/// Starts tasks to listen to and write to the websocket.
|
||||
fn start_websocket_listener_and_writer_tasks(
|
||||
&mut self,
|
||||
mut sink: impl Sink,
|
||||
stream: impl Stream,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
// TODO(alokedesai): Add a spawn_stream equivalent that runs on the background executor.
|
||||
ctx.spawn_stream_local(
|
||||
stream,
|
||||
|event_loop, message, _| {
|
||||
let message = match message {
|
||||
Ok(message) => message,
|
||||
Err(err) => {
|
||||
log::error!("Unable to receive item: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(bytes) = message.binary() else {
|
||||
log::error!("Received non binary message");
|
||||
return;
|
||||
};
|
||||
|
||||
event_loop.process_pty_bytes(bytes);
|
||||
},
|
||||
|_, _| {},
|
||||
);
|
||||
|
||||
let is_honor_ps1_enabled = *SessionSettings::as_ref(ctx).honor_ps1;
|
||||
|
||||
let receiver = self.event_loop_rx.clone();
|
||||
ctx.background_executor()
|
||||
.spawn(async move {
|
||||
if let Err(e) = Self::write_env_vars(&mut sink, is_honor_ps1_enabled).await {
|
||||
log::error!("Failed to write env vars to pty {e:?}");
|
||||
}
|
||||
if let Err(e) = Self::write_zsh_init_shell_script(&mut sink).await {
|
||||
log::error!("Failed to write zsh bootstrap bytes to pty {e:?}");
|
||||
}
|
||||
|
||||
while let Ok(message) = receiver.recv().await {
|
||||
match message {
|
||||
EventLoopMessage::Input(bytes) => {
|
||||
if let Err(e) = sink.send(Message::new_binary(bytes.to_vec())).await {
|
||||
log::error!("Failed to send message to network-backed PTY {e:?}");
|
||||
};
|
||||
}
|
||||
EventLoopMessage::Resize(size_info) => {
|
||||
let size_change = WindowSizeChange {
|
||||
width: size_info.columns as u32,
|
||||
height: size_info.rows as u32,
|
||||
width_px: size_info.pane_width_px().as_f32() as u32,
|
||||
height_px: size_info.pane_height_px().as_f32() as u32,
|
||||
};
|
||||
|
||||
let Ok(serialized) = serde_json::to_string(&size_change) else {
|
||||
log::error!("Error serializing window size change info");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Sending as a `Text` message implies that this is a
|
||||
// control channel message. The SSH proxy server should
|
||||
// make this distinction.
|
||||
if let Err(e) = sink.send(Message::new_text(serialized)).await {
|
||||
log::error!("Failed to send message to network-backed PTY {e:?}");
|
||||
};
|
||||
}
|
||||
// TODO(alokedesai): Implement shutdown on the network backed PTY.
|
||||
EventLoopMessage::Shutdown | EventLoopMessage::ChildExited => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Writes the ZSH init shell script to the "PTY", mimicking how we send the init shell script
|
||||
/// when there is a local pty:
|
||||
/// <https://github.com/warpdotdev/warp-internal/blob/747da2df83f2caa97e781ce284ceb226fb97a66c/app/src/terminal/local_tty/unix.rs#L338-L347>.
|
||||
async fn write_zsh_init_shell_script(sink: &mut impl Sink) -> anyhow::Result<()> {
|
||||
let zsh_init_shell_script = init_shell_script_for_shell(ShellType::Zsh, &crate::ASSETS);
|
||||
sink.send(Message::new_binary(
|
||||
zsh_init_shell_script.as_bytes().to_vec(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
sink.send(Message::new_binary(
|
||||
ShellType::Zsh.execute_command_bytes().to_vec(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes environment variables that should be defined in the session
|
||||
/// before bootstrapping. This is a subset of the environment variables
|
||||
/// defined in `app/src/terminal/local_tty/unix.rs` that are necessary in
|
||||
/// order to dogfood Warp on Web over the remote tty.
|
||||
async fn write_env_vars(
|
||||
sink: &mut impl Sink,
|
||||
is_honor_ps1_enabled: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let honor_ps1_env_var = format!(r#"WARP_HONOR_PS1="{}";"#, is_honor_ps1_enabled as u8);
|
||||
sink.send(Message::new_binary(honor_ps1_env_var.as_bytes().to_vec()))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_ws_connection(
|
||||
&mut self,
|
||||
connection: anyhow::Result<WebSocket>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let connection = match connection {
|
||||
Ok(connection) => connection,
|
||||
Err(e) => {
|
||||
log::error!("Failed to construct websocket connection: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
ctx.spawn(connection.split(), |me, (sink, stream), ctx| {
|
||||
me.start_websocket_listener_and_writer_tasks(sink, stream, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
/// Processes a byte slice through the `Processor`.
|
||||
fn process_pty_bytes(&mut self, bytes: &[u8]) {
|
||||
let mut terminal_model = self.terminal_model.lock();
|
||||
self.parser
|
||||
.parse_bytes(&mut *terminal_model, bytes, &mut io::sink());
|
||||
self.channel_event_listener.send_wakeup_event();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for EventLoop {
|
||||
type Event = ();
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
mod event_loop;
|
||||
mod terminal_manager;
|
||||
|
||||
pub use terminal_manager::TerminalManager;
|
||||
@@ -0,0 +1,196 @@
|
||||
use crate::ai::blocklist::InputConfig;
|
||||
use crate::context_chips::prompt_type::PromptType;
|
||||
use crate::pane_group::TerminalViewResources;
|
||||
use crate::persistence::ModelEvent;
|
||||
use crate::terminal::event_listener::ChannelEventListener;
|
||||
use crate::terminal::model::session::Sessions;
|
||||
use crate::terminal::remote_tty::event_loop::EventLoop;
|
||||
use crate::terminal::shell::{ShellName, ShellType};
|
||||
use crate::terminal::writeable_pty::pty_controller::{EventLoopSendError, EventLoopSender};
|
||||
use crate::terminal::writeable_pty::terminal_manager_util::{
|
||||
init_pty_controller_model, wire_up_pty_controller_with_view,
|
||||
};
|
||||
use crate::terminal::ShellLaunchState;
|
||||
use std::any::Any;
|
||||
|
||||
use crate::terminal::model_events::ModelEventDispatcher;
|
||||
use crate::terminal::writeable_pty::{self, Message};
|
||||
use crate::terminal::{terminal_manager, SizeInfo, TerminalModel, TerminalView};
|
||||
use async_channel::{Receiver, Sender, TrySendError};
|
||||
use parking_lot::FairMutex;
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::sync::Arc;
|
||||
use warpui::{AppContext, ModelHandle, ViewHandle, WindowId};
|
||||
|
||||
type PtyController = writeable_pty::PtyController<Sender<Message>>;
|
||||
|
||||
pub struct TerminalManager {
|
||||
model: Arc<FairMutex<TerminalModel>>,
|
||||
|
||||
// Store a reference to the PTYController and EventLoop so the UI framework doesn't end up
|
||||
// deallocating them because there are no strong references to the models.
|
||||
_pty_controller: ModelHandle<PtyController>,
|
||||
|
||||
_event_loop: ModelHandle<EventLoop>,
|
||||
|
||||
view: ViewHandle<TerminalView>,
|
||||
}
|
||||
|
||||
impl TerminalManager {
|
||||
/// Creates a terminal manager model that feeds bytes to/from a remote PTY.
|
||||
pub fn create_model(
|
||||
resources: TerminalViewResources,
|
||||
initial_size: Vector2F,
|
||||
model_event_sender: Option<SyncSender<ModelEvent>>,
|
||||
window_id: WindowId,
|
||||
initial_input_config: Option<InputConfig>,
|
||||
ctx: &mut AppContext,
|
||||
) -> ModelHandle<Box<dyn crate::terminal::TerminalManager>> {
|
||||
// Create all the necessary channels we need for communication.
|
||||
let (wakeups_tx, wakeups_rx) = async_channel::unbounded();
|
||||
let (events_tx, events_rx) = async_channel::unbounded();
|
||||
let (executor_command_tx, executor_command_rx) = async_channel::unbounded();
|
||||
|
||||
// Use an empty pty reads broadcaster since we don't need to broadcast any PTY bytes for the
|
||||
// network-backed PTY. We use 1 instead of 0 here because `async_broadcast` internally
|
||||
// asserts that the capacity is at least 1.
|
||||
let (pty_reads_tx, _pty_reads_rx) = async_broadcast::broadcast(1);
|
||||
|
||||
let channel_event_proxy = ChannelEventListener::new(wakeups_tx, events_tx, pty_reads_tx);
|
||||
|
||||
// Initialize the sessions model.
|
||||
let sessions: ModelHandle<Sessions> =
|
||||
ctx.add_model(|ctx| Sessions::new(executor_command_tx, ctx));
|
||||
|
||||
let model_events =
|
||||
ctx.add_model(|ctx| ModelEventDispatcher::new(events_rx, sessions.clone(), ctx));
|
||||
|
||||
// Create the terminal model.
|
||||
let model = terminal_manager::create_terminal_model(
|
||||
None, /* startup_directory */
|
||||
None, /* restored_blocks */
|
||||
initial_size,
|
||||
channel_event_proxy.clone(),
|
||||
// TODO(alokedesai): Add support for other shells within the network-backed pty.
|
||||
ShellLaunchState::ShellSpawned {
|
||||
available_shell: None,
|
||||
display_name: ShellName::blank(),
|
||||
shell_type: ShellType::Zsh,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
|
||||
let size_info = *model.block_list().size();
|
||||
let colors = model.colors();
|
||||
let model = Arc::new(FairMutex::new(model));
|
||||
|
||||
let (event_loop_tx, event_loop_rx) = async_channel::unbounded();
|
||||
|
||||
let event_loop = Self::create_and_start_event_loop(
|
||||
model.clone(),
|
||||
channel_event_proxy.clone(),
|
||||
event_loop_rx,
|
||||
size_info,
|
||||
ctx,
|
||||
);
|
||||
|
||||
// Initialize the PtyController.
|
||||
let pty_controller = init_pty_controller_model(
|
||||
event_loop_tx.clone(),
|
||||
executor_command_rx,
|
||||
model_events.clone(),
|
||||
sessions.clone(),
|
||||
model.clone(),
|
||||
ctx,
|
||||
);
|
||||
|
||||
let cloned_model = model.clone();
|
||||
let prompt_type =
|
||||
ctx.add_model(|ctx| PromptType::new_dynamic_from_sessions(sessions.clone(), ctx));
|
||||
let view = ctx.add_typed_action_view(window_id, |ctx| {
|
||||
TerminalView::new(
|
||||
resources,
|
||||
wakeups_rx,
|
||||
model_events.clone(),
|
||||
cloned_model,
|
||||
sessions.clone(),
|
||||
size_info,
|
||||
colors,
|
||||
model_event_sender.clone(),
|
||||
prompt_type,
|
||||
initial_input_config,
|
||||
None, // conversation_restoration - not used for remote
|
||||
None, // inactive_pty_reads_rx
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
wire_up_pty_controller_with_view(
|
||||
&pty_controller,
|
||||
&view,
|
||||
model.clone(),
|
||||
sessions,
|
||||
model_event_sender,
|
||||
ctx,
|
||||
);
|
||||
|
||||
// Create the terminal manager itself.
|
||||
let terminal_manager = Self {
|
||||
model,
|
||||
view,
|
||||
_pty_controller: pty_controller,
|
||||
_event_loop: event_loop,
|
||||
};
|
||||
|
||||
ctx.add_model(|_ctx| {
|
||||
let manager: Box<dyn crate::terminal::TerminalManager> = Box::new(terminal_manager);
|
||||
manager
|
||||
})
|
||||
}
|
||||
|
||||
fn create_and_start_event_loop(
|
||||
terminal_model: Arc<FairMutex<TerminalModel>>,
|
||||
channel_event_listener: ChannelEventListener,
|
||||
message_receiver: Receiver<Message>,
|
||||
size_info: SizeInfo,
|
||||
ctx: &mut AppContext,
|
||||
) -> ModelHandle<EventLoop> {
|
||||
ctx.add_model(|ctx| {
|
||||
EventLoop::start(
|
||||
terminal_model,
|
||||
message_receiver,
|
||||
channel_event_listener,
|
||||
size_info,
|
||||
ctx,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl super::super::TerminalManager for TerminalManager {
|
||||
fn model(&self) -> Arc<FairMutex<TerminalModel>> {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
fn view(&self) -> ViewHandle<TerminalView> {
|
||||
self.view.clone()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl EventLoopSender for Sender<Message> {
|
||||
fn send(&self, message: Message) -> Result<(), EventLoopSendError> {
|
||||
self.try_send(message).map_err(|err| match err {
|
||||
TrySendError::Closed(_) => EventLoopSendError::Disconnected,
|
||||
TrySendError::Full(_) => EventLoopSendError::Other(err.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user