Files
yaak-mountain-loop/crates-tauri/yaak-app/src/lib.rs
T
Gregory Schier 4d75b8ef06 Surface gRPC message deserialization errors to UI
Previously, when a gRPC streaming message failed to deserialize (e.g., wrong
type like int instead of string), the error was silently logged and the message
was dropped. Now errors are surfaced to the UI as GrpcEventType::Error events.

Changed the streaming/client_streaming methods to accept an on_message callback
that handles both success (logs ClientMessage) and error (logs Error) cases,
rather than logging the client message prematurely before deserialization.
2026-01-10 14:57:28 -08:00

1895 lines
70 KiB
Rust

extern crate core;
use crate::encoding::read_response_body;
use crate::error::Error::GenericError;
use crate::error::Result;
use crate::grpc::{build_metadata, metadata_to_map, resolve_grpc_request};
use crate::http_request::{resolve_http_request, send_http_request};
use crate::import::import_data;
use crate::models_ext::{BlobManagerExt, QueryManagerExt};
use crate::notifications::YaakNotifier;
use crate::render::{render_grpc_request, render_template};
use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
use crate::uri_scheme::handle_deep_link;
use error::Result as YaakResult;
use eventsource_client::{EventParser, SSE};
use log::{debug, error, info, warn};
use std::collections::HashMap;
use std::fs::File;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use std::{fs, panic};
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Emitter, RunEvent, State, WebviewWindow, is_dev};
use tauri::{Listener, Runtime};
use tauri::{Manager, WindowEvent};
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_log::fern::colors::ColoredLevelConfig;
use tauri_plugin_log::{Builder, Target, TargetKind, log};
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
use tokio::sync::Mutex;
use tokio::task::block_in_place;
use tokio::time;
use yaak_crypto::manager::EncryptionManager;
use yaak_grpc::manager::{GrpcConfig, GrpcHandle};
use yaak_grpc::{Code, ServiceDefinition, serialize_message};
use yaak_mac_window::AppHandleMacWindowExt;
use yaak_models::models::{
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState,
Plugin, Workspace, WorkspaceMeta,
};
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{
CallFolderActionArgs, CallFolderActionRequest, CallGrpcRequestActionArgs,
CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest,
CallWebsocketRequestActionArgs, CallWebsocketRequestActionRequest, CallWorkspaceActionArgs,
CallWorkspaceActionRequest, Color, FilterResponse, GetFolderActionsResponse,
GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse,
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse,
GetWebsocketRequestActionsResponse, GetWorkspaceActionsResponse, InternalEvent,
InternalEventPayload, JsonPrimitive, PluginContext, RenderPurpose, ShowToastRequest,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_meta::PluginMetadata;
use yaak_plugins::template_callback::PluginTemplateCallback;
use yaak_sse::sse::ServerSentEvent;
use yaak_tauri_utils::window::WorkspaceWindowTrait;
use yaak_templates::format_json::format_json;
use yaak_templates::{RenderErrorBehavior, RenderOptions, Tokens, transform_args};
use yaak_tls::find_client_certificate;
mod commands;
mod encoding;
mod error;
mod git_ext;
mod grpc;
mod history;
mod http_request;
mod import;
mod models_ext;
mod notifications;
mod plugin_events;
mod plugins_ext;
mod render;
mod sync_ext;
mod updates;
mod uri_scheme;
mod window;
mod window_menu;
mod ws_ext;
/// Extension trait for easily creating a PluginContext from a WebviewWindow
pub trait PluginContextExt<R: Runtime> {
fn plugin_context(&self) -> PluginContext;
}
impl<R: Runtime> PluginContextExt<R> for WebviewWindow<R> {
fn plugin_context(&self) -> PluginContext {
PluginContext::new(Some(self.label().to_string()), self.workspace_id())
}
}
#[derive(serde::Serialize)]
#[serde(default, rename_all = "camelCase")]
struct AppMetaData {
is_dev: bool,
version: String,
name: String,
app_data_dir: String,
app_log_dir: String,
vendored_plugin_dir: String,
feature_updater: bool,
feature_license: bool,
}
#[tauri::command]
async fn cmd_metadata(app_handle: AppHandle) -> YaakResult<AppMetaData> {
let app_data_dir = app_handle.path().app_data_dir()?;
let app_log_dir = app_handle.path().app_log_dir()?;
let vendored_plugin_dir =
app_handle.path().resolve("vendored/plugins", BaseDirectory::Resource)?;
Ok(AppMetaData {
is_dev: is_dev(),
version: app_handle.package_info().version.to_string(),
name: app_handle.package_info().name.to_string(),
app_data_dir: app_data_dir.to_string_lossy().to_string(),
app_log_dir: app_log_dir.to_string_lossy().to_string(),
vendored_plugin_dir: vendored_plugin_dir.to_string_lossy().to_string(),
feature_license: cfg!(feature = "license"),
feature_updater: cfg!(feature = "updater"),
})
}
#[tauri::command]
async fn cmd_template_tokens_to_string<R: Runtime>(
window: WebviewWindow<R>,
app_handle: AppHandle<R>,
tokens: Tokens,
) -> YaakResult<String> {
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
let cb = PluginTemplateCallback::new(
plugin_manager,
encryption_manager,
&PluginContext::new(Some(window.label().to_string()), window.workspace_id()),
RenderPurpose::Preview,
);
let new_tokens = transform_args(tokens, &cb)?;
Ok(new_tokens.to_string())
}
#[tauri::command]
async fn cmd_render_template<R: Runtime>(
window: WebviewWindow<R>,
app_handle: AppHandle<R>,
template: &str,
workspace_id: &str,
environment_id: Option<&str>,
purpose: Option<RenderPurpose>,
ignore_error: Option<bool>,
) -> YaakResult<String> {
let environment_chain =
app_handle.db().resolve_environments(workspace_id, None, environment_id)?;
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
let result = render_template(
template,
environment_chain,
&PluginTemplateCallback::new(
plugin_manager,
encryption_manager,
&PluginContext::new(Some(window.label().to_string()), window.workspace_id()),
purpose.unwrap_or(RenderPurpose::Preview),
),
&RenderOptions {
error_behavior: match ignore_error {
Some(true) => RenderErrorBehavior::ReturnEmpty,
_ => RenderErrorBehavior::Throw,
},
},
)
.await?;
Ok(result)
}
#[tauri::command]
async fn cmd_dismiss_notification<R: Runtime>(
window: WebviewWindow<R>,
notification_id: &str,
yaak_notifier: State<'_, Mutex<YaakNotifier>>,
) -> YaakResult<()> {
Ok(yaak_notifier.lock().await.seen(&window, notification_id).await?)
}
#[tauri::command]
async fn cmd_grpc_reflect<R: Runtime>(
request_id: &str,
environment_id: Option<&str>,
proto_files: Vec<String>,
skip_cache: Option<bool>,
window: WebviewWindow<R>,
app_handle: AppHandle<R>,
grpc_handle: State<'_, Mutex<GrpcHandle>>,
) -> YaakResult<Vec<ServiceDefinition>> {
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?;
let environment_chain = app_handle.db().resolve_environments(
&unrendered_request.workspace_id,
unrendered_request.folder_id.as_deref(),
environment_id,
)?;
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
let req = render_grpc_request(
&resolved_request,
environment_chain,
&PluginTemplateCallback::new(
plugin_manager,
encryption_manager,
&PluginContext::new(Some(window.label().to_string()), window.workspace_id()),
RenderPurpose::Send,
),
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
)
.await?;
let uri = safe_uri(&req.url);
let metadata = build_metadata(&window, &req, &auth_context_id).await?;
let settings = window.db().get_settings();
let client_certificate =
find_client_certificate(req.url.as_str(), &settings.client_certificates);
Ok(grpc_handle
.lock()
.await
.services(
&req.id,
&uri,
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
&metadata,
workspace.setting_validate_certificates,
client_certificate,
skip_cache.unwrap_or(false),
)
.await
.map_err(|e| GenericError(e.to_string()))?)
}
#[tauri::command]
async fn cmd_grpc_go<R: Runtime>(
request_id: &str,
environment_id: Option<&str>,
proto_files: Vec<String>,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
grpc_handle: State<'_, Mutex<GrpcHandle>>,
) -> YaakResult<String> {
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?;
let environment_chain = app_handle.db().resolve_environments(
&unrendered_request.workspace_id,
unrendered_request.folder_id.as_deref(),
environment_id,
)?;
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
let request = render_grpc_request(
&resolved_request,
environment_chain.clone(),
&PluginTemplateCallback::new(
plugin_manager.clone(),
encryption_manager.clone(),
&PluginContext::new(Some(window.label().to_string()), window.workspace_id()),
RenderPurpose::Send,
),
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
)
.await?;
let metadata = build_metadata(&window, &request, &auth_context_id).await?;
// Find matching client certificate for this URL
let settings = app_handle.db().get_settings();
let client_cert = find_client_certificate(&request.url, &settings.client_certificates);
let conn = app_handle.db().upsert_grpc_connection(
&GrpcConnection {
workspace_id: request.workspace_id.clone(),
request_id: request.id.clone(),
status: -1,
elapsed: 0,
state: GrpcConnectionState::Initialized,
url: request.url.clone(),
..Default::default()
},
&UpdateSource::from_window_label(window.label()),
)?;
let conn_id = conn.id.clone();
let base_msg = GrpcEvent {
workspace_id: request.clone().workspace_id,
request_id: request.clone().id,
connection_id: conn.clone().id,
..Default::default()
};
let (in_msg_tx, in_msg_rx) = tauri::async_runtime::channel::<String>(16);
let maybe_in_msg_tx = std::sync::Mutex::new(Some(in_msg_tx.clone()));
let (cancelled_tx, mut cancelled_rx) = tokio::sync::watch::channel(false);
let uri = safe_uri(&request.url);
let in_msg_stream = tokio_stream::wrappers::ReceiverStream::new(in_msg_rx);
let (service, method) = {
let req = request.clone();
match (req.service, req.method) {
(Some(service), Some(method)) => (service, method),
_ => return Err(GenericError("Service and method are required".to_string())),
}
};
let start = std::time::Instant::now();
let connection = grpc_handle
.lock()
.await
.connect(
&request.clone().id,
uri.as_str(),
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
&metadata,
workspace.setting_validate_certificates,
client_cert.clone(),
)
.await;
let connection = match connection {
Ok(c) => c,
Err(err) => {
app_handle.db().upsert_grpc_connection(
&GrpcConnection {
elapsed: start.elapsed().as_millis() as i32,
error: Some(err.to_string()),
state: GrpcConnectionState::Closed,
..conn.clone()
},
&UpdateSource::from_window_label(window.label()),
)?;
return Ok(conn_id);
}
};
let method_desc =
connection.method(&service, &method).await.map_err(|e| GenericError(e.to_string()))?;
#[derive(serde::Deserialize)]
enum IncomingMsg {
Message(String),
Cancel,
Commit,
}
let cb = {
let cancelled_rx = cancelled_rx.clone();
let environment_chain = environment_chain.clone();
let window = window.clone();
let plugin_manager = plugin_manager.clone();
let encryption_manager = encryption_manager.clone();
move |ev: tauri::Event| {
if *cancelled_rx.borrow() {
// Stream is canceled
return;
}
let mut maybe_in_msg_tx = maybe_in_msg_tx.lock().expect("previous holder not to panic");
let in_msg_tx = if let Some(in_msg_tx) = maybe_in_msg_tx.as_ref() {
in_msg_tx
} else {
// This would mean that the stream is already committed because
// we have already dropped the sending half
return;
};
match serde_json::from_str::<IncomingMsg>(ev.payload()) {
Ok(IncomingMsg::Message(msg)) => {
let window = window.clone();
let environment_chain = environment_chain.clone();
let plugin_manager = plugin_manager.clone();
let encryption_manager = encryption_manager.clone();
let msg = block_in_place(|| {
tauri::async_runtime::block_on(async {
let result = render_template(
msg.as_str(),
environment_chain,
&PluginTemplateCallback::new(
plugin_manager,
encryption_manager,
&PluginContext::new(
Some(window.label().to_string()),
window.workspace_id(),
),
RenderPurpose::Send,
),
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
)
.await;
result.expect("Failed to render template")
})
});
in_msg_tx.try_send(msg.clone()).unwrap();
}
Ok(IncomingMsg::Commit) => {
maybe_in_msg_tx.take();
}
Ok(IncomingMsg::Cancel) => {
cancelled_tx.send_replace(true);
}
Err(e) => {
error!("Failed to parse gRPC message: {:?}", e);
}
}
}
};
let event_handler = app_handle.listen_any(format!("grpc_client_msg_{}", conn.id).as_str(), cb);
let grpc_listen = {
let window = window.clone();
let app_handle = app_handle.clone();
let base_event = base_msg.clone();
let environment_chain = environment_chain.clone();
let req = request.clone();
let msg = if req.message.is_empty() { "{}".to_string() } else { req.message };
let msg = render_template(
msg.as_str(),
environment_chain,
&PluginTemplateCallback::new(
plugin_manager.clone(),
encryption_manager.clone(),
&PluginContext::new(Some(window.label().to_string()), window.workspace_id()),
RenderPurpose::Send,
),
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
)
.await?;
app_handle.db().upsert_grpc_event(
&GrpcEvent {
content: format!("Connecting to {}", req.url),
event_type: GrpcEventType::ConnectionStart,
metadata: metadata.clone(),
..base_event.clone()
},
&UpdateSource::from_window_label(window.label()),
)?;
async move {
// Create callback for streaming methods that handles both success and error
let on_message = {
let app_handle = app_handle.clone();
let base_event = base_event.clone();
let window_label = window.label().to_string();
move |result: std::result::Result<String, String>| match result {
Ok(msg) => {
let _ = app_handle.db().upsert_grpc_event(
&GrpcEvent {
content: msg,
event_type: GrpcEventType::ClientMessage,
..base_event.clone()
},
&UpdateSource::from_window_label(&window_label),
);
}
Err(error) => {
let _ = app_handle.db().upsert_grpc_event(
&GrpcEvent {
content: format!("Failed to send message: {}", error),
event_type: GrpcEventType::Error,
..base_event.clone()
},
&UpdateSource::from_window_label(&window_label),
);
}
}
};
let (maybe_stream, maybe_msg) =
match (method_desc.is_client_streaming(), method_desc.is_server_streaming()) {
(true, true) => (
Some(
connection
.streaming(
&service,
&method,
in_msg_stream,
&metadata,
client_cert,
on_message.clone(),
)
.await,
),
None,
),
(true, false) => (
None,
Some(
connection
.client_streaming(
&service,
&method,
in_msg_stream,
&metadata,
client_cert,
on_message.clone(),
)
.await,
),
),
(false, true) => (
Some(connection.server_streaming(&service, &method, &msg, &metadata).await),
None,
),
(false, false) => (
None,
Some(
connection.unary(&service, &method, &msg, &metadata, client_cert).await,
),
),
};
if !method_desc.is_client_streaming() {
app_handle
.db()
.upsert_grpc_event(
&GrpcEvent {
event_type: GrpcEventType::ClientMessage,
content: msg,
..base_event.clone()
},
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
}
match maybe_msg {
Some(Ok(msg)) => {
app_handle
.db()
.upsert_grpc_event(
&GrpcEvent {
metadata: metadata_to_map(msg.metadata().clone()),
content: if msg.metadata().len() == 0 {
"Received response"
} else {
"Received response with metadata"
}
.to_string(),
event_type: GrpcEventType::Info,
..base_event.clone()
},
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
app_handle
.db()
.upsert_grpc_event(
&GrpcEvent {
content: serialize_message(&msg.into_inner()).unwrap(),
event_type: GrpcEventType::ServerMessage,
..base_event.clone()
},
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
app_handle
.db()
.upsert_grpc_event(
&GrpcEvent {
content: "Connection complete".to_string(),
event_type: GrpcEventType::ConnectionEnd,
status: Some(Code::Ok as i32),
..base_event.clone()
},
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
}
Some(Err(yaak_grpc::error::Error::GrpcStreamError(e))) => {
app_handle
.db()
.upsert_grpc_event(
&(match e.status {
Some(s) => GrpcEvent {
error: Some(s.message().to_string()),
status: Some(s.code() as i32),
content: "Failed to connect".to_string(),
metadata: metadata_to_map(s.metadata().clone()),
event_type: GrpcEventType::ConnectionEnd,
..base_event.clone()
},
None => GrpcEvent {
error: Some(e.message),
status: Some(Code::Unknown as i32),
content: "Failed to connect".to_string(),
event_type: GrpcEventType::ConnectionEnd,
..base_event.clone()
},
}),
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
}
Some(Err(e)) => {
app_handle
.db()
.upsert_grpc_event(
&GrpcEvent {
error: Some(e.to_string()),
status: Some(Code::Unknown as i32),
content: "Failed to connect".to_string(),
event_type: GrpcEventType::ConnectionEnd,
..base_event.clone()
},
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
}
None => {
// Server streaming doesn't return the initial message
}
}
let mut stream = match maybe_stream {
Some(Ok(stream)) => {
app_handle
.db()
.upsert_grpc_event(
&GrpcEvent {
metadata: metadata_to_map(stream.metadata().clone()),
content: if stream.metadata().len() == 0 {
"Received response"
} else {
"Received response with metadata"
}
.to_string(),
event_type: GrpcEventType::Info,
..base_event.clone()
},
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
stream.into_inner()
}
Some(Err(yaak_grpc::error::Error::GrpcStreamError(e))) => {
warn!("GRPC stream error {e:?}");
app_handle
.db()
.upsert_grpc_event(
&(match e.status {
Some(s) => GrpcEvent {
error: Some(s.message().to_string()),
status: Some(s.code() as i32),
content: "Failed to connect".to_string(),
metadata: metadata_to_map(s.metadata().clone()),
event_type: GrpcEventType::ConnectionEnd,
..base_event.clone()
},
None => GrpcEvent {
error: Some(e.message),
status: Some(Code::Unknown as i32),
content: "Failed to connect".to_string(),
event_type: GrpcEventType::ConnectionEnd,
..base_event.clone()
},
}),
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
return;
}
Some(Err(e)) => {
app_handle
.db()
.upsert_grpc_event(
&GrpcEvent {
error: Some(e.to_string()),
status: Some(Code::Unknown as i32),
content: "Failed to connect".to_string(),
event_type: GrpcEventType::ConnectionEnd,
..base_event.clone()
},
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
return;
}
None => return,
};
loop {
match stream.message().await {
Ok(Some(msg)) => {
let message = serialize_message(&msg).unwrap();
app_handle
.db()
.upsert_grpc_event(
&GrpcEvent {
content: message,
event_type: GrpcEventType::ServerMessage,
..base_event.clone()
},
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
}
Ok(None) => {
let trailers =
stream.trailers().await.unwrap_or_default().unwrap_or_default();
app_handle
.db()
.upsert_grpc_event(
&GrpcEvent {
content: "Connection complete".to_string(),
status: Some(Code::Ok as i32),
metadata: metadata_to_map(trailers),
event_type: GrpcEventType::ConnectionEnd,
..base_event.clone()
},
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
break;
}
Err(status) => {
app_handle
.db()
.upsert_grpc_event(
&GrpcEvent {
content: status.to_string(),
status: Some(status.code() as i32),
metadata: metadata_to_map(status.metadata().clone()),
event_type: GrpcEventType::ConnectionEnd,
..base_event.clone()
},
&UpdateSource::from_window_label(window.label()),
)
.unwrap();
}
}
}
}
};
{
let conn_id = conn_id.clone();
tauri::async_runtime::spawn(async move {
let w = app_handle.clone();
tokio::select! {
_ = grpc_listen => {
let events = w.db().list_grpc_events(&conn_id).unwrap();
let closed_event = events
.iter()
.find(|e| GrpcEventType::ConnectionEnd == e.event_type);
let closed_status = closed_event.and_then(|e| e.status).unwrap_or(Code::Unavailable as i32);
w.with_tx(|c| {
c.upsert_grpc_connection(
&GrpcConnection{
elapsed: start.elapsed().as_millis() as i32,
status: closed_status,
state: GrpcConnectionState::Closed,
..c.get_grpc_connection( &conn_id).unwrap().clone()
},
&UpdateSource::from_window_label(window.label()),
)
}).unwrap();
},
_ = cancelled_rx.changed() => {
w.db().upsert_grpc_event(
&GrpcEvent {
content: "Cancelled".to_string(),
event_type: GrpcEventType::ConnectionEnd,
status: Some(Code::Cancelled as i32),
..base_msg.clone()
},
&UpdateSource::from_window_label(window.label()),
).unwrap();
w.with_tx(|c| {
c.upsert_grpc_connection(
&GrpcConnection{
elapsed: start.elapsed().as_millis() as i32,
status: Code::Cancelled as i32,
state: GrpcConnectionState::Closed,
..c.get_grpc_connection( &conn_id).unwrap().clone()
},
&UpdateSource::from_window_label(window.label()),
)
}).unwrap();
},
}
w.unlisten(event_handler);
});
};
Ok(conn.id)
}
#[tauri::command]
async fn cmd_restart<R: Runtime>(app_handle: AppHandle<R>) -> YaakResult<()> {
app_handle.request_restart();
Ok(())
}
#[tauri::command]
async fn cmd_send_ephemeral_request<R: Runtime>(
mut request: HttpRequest,
environment_id: Option<&str>,
cookie_jar_id: Option<&str>,
window: WebviewWindow,
app_handle: AppHandle<R>,
) -> YaakResult<HttpResponse> {
let response = HttpResponse::default();
request.id = "".to_string();
let environment = match environment_id {
Some(id) => Some(app_handle.db().get_environment(id)?),
None => None,
};
let cookie_jar = match cookie_jar_id {
Some(id) => Some(app_handle.db().get_cookie_jar(id)?),
None => None,
};
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
window.listen_any(format!("cancel_http_response_{}", response.id), move |_event| {
if let Err(e) = cancel_tx.send(true) {
warn!("Failed to send cancel event for ephemeral request {e:?}");
}
});
send_http_request(&window, &request, &response, environment, cookie_jar, &mut cancel_rx).await
}
#[tauri::command]
async fn cmd_format_json(text: &str) -> YaakResult<String> {
Ok(format_json(text, " "))
}
#[tauri::command]
async fn cmd_http_response_body<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
response: HttpResponse,
filter: Option<&str>,
) -> YaakResult<FilterResponse> {
let body_path = match response.body_path {
None => {
return Ok(FilterResponse { content: String::new(), error: None });
}
Some(p) => p,
};
let content_type = response
.headers
.iter()
.find_map(|h| {
if h.name.eq_ignore_ascii_case("content-type") { Some(h.value.as_str()) } else { None }
})
.unwrap_or_default();
let body = read_response_body(&body_path, content_type)
.await
.ok_or(GenericError("Failed to find response body".to_string()))?;
match filter {
Some(filter) if !filter.is_empty() => Ok(plugin_manager
.filter_data(&window.plugin_context(), filter, &body, content_type)
.await?),
_ => Ok(FilterResponse { content: body, error: None }),
}
}
#[tauri::command]
async fn cmd_http_request_body<R: Runtime>(
app_handle: AppHandle<R>,
response_id: &str,
) -> YaakResult<Option<Vec<u8>>> {
let body_id = format!("{}.request", response_id);
let chunks = app_handle.blobs().get_chunks(&body_id)?;
if chunks.is_empty() {
return Ok(None);
}
// Concatenate all chunks
let body: Vec<u8> = chunks.into_iter().flat_map(|c| c.data).collect();
Ok(Some(body))
}
#[tauri::command]
async fn cmd_get_sse_events(file_path: &str) -> YaakResult<Vec<ServerSentEvent>> {
let body = fs::read(file_path)?;
let mut event_parser = EventParser::new();
event_parser.process_bytes(body.into())?;
let mut events = Vec::new();
while let Some(e) = event_parser.get_event() {
if let SSE::Event(e) = e {
events.push(ServerSentEvent {
event_type: e.event_type,
data: e.data,
id: e.id,
retry: e.retry,
});
}
}
Ok(events)
}
#[tauri::command]
async fn cmd_get_http_response_events<R: Runtime>(
app_handle: AppHandle<R>,
response_id: &str,
) -> YaakResult<Vec<HttpResponseEvent>> {
let events: Vec<HttpResponseEvent> = app_handle.db().list_http_response_events(response_id)?;
Ok(events)
}
#[tauri::command]
async fn cmd_import_data<R: Runtime>(
window: WebviewWindow<R>,
file_path: &str,
) -> YaakResult<BatchUpsertResult> {
import_data(&window, file_path).await
}
#[tauri::command]
async fn cmd_http_request_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<GetHttpRequestActionsResponse>> {
Ok(plugin_manager.get_http_request_actions(&window.plugin_context()).await?)
}
#[tauri::command]
async fn cmd_websocket_request_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<GetWebsocketRequestActionsResponse>> {
Ok(plugin_manager.get_websocket_request_actions(&window.plugin_context()).await?)
}
#[tauri::command]
async fn cmd_call_websocket_request_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallWebsocketRequestActionRequest,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<()> {
let websocket_request = window.db().get_websocket_request(&req.args.websocket_request.id)?;
Ok(plugin_manager
.call_websocket_request_action(
&window.plugin_context(),
CallWebsocketRequestActionRequest {
args: CallWebsocketRequestActionArgs { websocket_request },
..req
},
)
.await?)
}
#[tauri::command]
async fn cmd_workspace_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<GetWorkspaceActionsResponse>> {
Ok(plugin_manager.get_workspace_actions(&window.plugin_context()).await?)
}
#[tauri::command]
async fn cmd_call_workspace_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallWorkspaceActionRequest,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<()> {
let workspace = window.db().get_workspace(&req.args.workspace.id)?;
Ok(plugin_manager
.call_workspace_action(
&window.plugin_context(),
CallWorkspaceActionRequest { args: CallWorkspaceActionArgs { workspace }, ..req },
)
.await?)
}
#[tauri::command]
async fn cmd_folder_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<GetFolderActionsResponse>> {
Ok(plugin_manager.get_folder_actions(&window.plugin_context()).await?)
}
#[tauri::command]
async fn cmd_call_folder_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallFolderActionRequest,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<()> {
let folder = window.db().get_folder(&req.args.folder.id)?;
Ok(plugin_manager
.call_folder_action(
&window.plugin_context(),
CallFolderActionRequest { args: CallFolderActionArgs { folder }, ..req },
)
.await?)
}
#[tauri::command]
async fn cmd_grpc_request_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<GetGrpcRequestActionsResponse>> {
Ok(plugin_manager.get_grpc_request_actions(&window.plugin_context()).await?)
}
#[tauri::command]
async fn cmd_template_function_summaries<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<GetTemplateFunctionSummaryResponse>> {
let results = plugin_manager.get_template_function_summaries(&window.plugin_context()).await?;
Ok(results)
}
#[tauri::command]
async fn cmd_template_function_config<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
function_name: &str,
values: HashMap<String, JsonPrimitive>,
model: AnyModel,
_environment_id: Option<&str>,
) -> YaakResult<GetTemplateFunctionConfigResponse> {
Ok(plugin_manager
.get_template_function_config(&window.plugin_context(), function_name, values, model.id())
.await?)
}
#[tauri::command]
async fn cmd_get_http_authentication_summaries<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<GetHttpAuthenticationSummaryResponse>> {
let results =
plugin_manager.get_http_authentication_summaries(&window.plugin_context()).await?;
Ok(results.into_iter().map(|(_, a)| a).collect())
}
#[tauri::command]
async fn cmd_get_http_authentication_config<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
auth_name: &str,
values: HashMap<String, JsonPrimitive>,
model: AnyModel,
_environment_id: Option<&str>,
) -> YaakResult<GetHttpAuthenticationConfigResponse> {
Ok(plugin_manager
.get_http_authentication_config(&window.plugin_context(), auth_name, values, model.id())
.await?)
}
#[tauri::command]
async fn cmd_call_http_request_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallHttpRequestActionRequest,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<()> {
Ok(plugin_manager
.call_http_request_action(
&window.plugin_context(),
CallHttpRequestActionRequest {
args: CallHttpRequestActionArgs {
http_request: resolve_http_request(&window, &req.args.http_request)?.0,
..req.args
},
..req
},
)
.await?)
}
#[tauri::command]
async fn cmd_call_grpc_request_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallGrpcRequestActionRequest,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<()> {
Ok(plugin_manager
.call_grpc_request_action(
&window.plugin_context(),
CallGrpcRequestActionRequest {
args: CallGrpcRequestActionArgs {
grpc_request: resolve_grpc_request(&window, &req.args.grpc_request)?.0,
..req.args
},
..req
},
)
.await?)
}
#[tauri::command]
async fn cmd_call_http_authentication_action<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
auth_name: &str,
action_index: i32,
values: HashMap<String, JsonPrimitive>,
model: AnyModel,
_environment_id: Option<&str>,
) -> YaakResult<()> {
Ok(plugin_manager
.call_http_authentication_action(
&window.plugin_context(),
auth_name,
action_index,
values,
&model.id(),
)
.await?)
}
#[tauri::command]
async fn cmd_curl_to_request<R: Runtime>(
window: WebviewWindow<R>,
command: &str,
plugin_manager: State<'_, PluginManager>,
workspace_id: &str,
) -> YaakResult<HttpRequest> {
let import_result = plugin_manager.import_data(&window.plugin_context(), command).await?;
Ok(import_result
.resources
.http_requests
.get(0)
.ok_or(GenericError("No curl command found".to_string()))
.map(|r| {
let mut request = r.clone();
request.workspace_id = workspace_id.into();
request.id = "".to_string();
request
})?)
}
#[tauri::command]
async fn cmd_export_data<R: Runtime>(
app_handle: AppHandle<R>,
export_path: &str,
workspace_ids: Vec<&str>,
include_private_environments: bool,
) -> YaakResult<()> {
let db = app_handle.db();
let version = app_handle.package_info().version.to_string();
let export_data =
get_workspace_export_resources(&db, &version, workspace_ids, include_private_environments)?;
let f = File::options()
.create(true)
.truncate(true)
.write(true)
.open(export_path)
.expect("Unable to create file");
serde_json::to_writer_pretty(&f, &export_data)
.map_err(|e| GenericError(e.to_string()))
.expect("Failed to write");
f.sync_all().expect("Failed to sync");
Ok(())
}
#[tauri::command]
async fn cmd_save_response<R: Runtime>(
app_handle: AppHandle<R>,
response_id: &str,
filepath: &str,
) -> YaakResult<()> {
let response = app_handle.db().get_http_response(response_id)?;
let body_path =
response.body_path.ok_or(GenericError("Response does not have a body".to_string()))?;
fs::copy(body_path, filepath).map_err(|e| GenericError(e.to_string()))?;
Ok(())
}
#[tauri::command]
async fn cmd_send_folder<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
environment_id: Option<String>,
cookie_jar_id: Option<String>,
folder_id: &str,
) -> YaakResult<()> {
let requests = app_handle.db().list_http_requests_for_folder_recursive(folder_id)?;
for request in requests {
let app_handle = app_handle.clone();
let window = window.clone();
let environment_id = environment_id.clone();
let cookie_jar_id = cookie_jar_id.clone();
tokio::spawn(async move {
let _ = cmd_send_http_request(
app_handle,
window,
environment_id.as_deref(),
cookie_jar_id.as_deref(),
request,
)
.await;
});
}
Ok(())
}
#[tauri::command]
async fn cmd_send_http_request<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
environment_id: Option<&str>,
cookie_jar_id: Option<&str>,
// NOTE: We receive the entire request because to account for the race
// condition where the user may have just edited a field before sending
// that has not yet been saved in the DB.
request: HttpRequest,
) -> YaakResult<HttpResponse> {
let blobs = app_handle.blob_manager();
let response = app_handle.db().upsert_http_response(
&HttpResponse {
request_id: request.id.clone(),
workspace_id: request.workspace_id.clone(),
..Default::default()
},
&UpdateSource::from_window_label(window.label()),
&blobs,
)?;
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
app_handle.listen_any(format!("cancel_http_response_{}", response.id), move |_event| {
if let Err(e) = cancel_tx.send(true) {
warn!("Failed to send cancel event for request {e:?}");
}
});
let environment = match environment_id {
Some(id) => match app_handle.db().get_environment(id) {
Ok(env) => Some(env),
Err(e) => {
warn!("Failed to find environment by id {id} {}", e);
None
}
},
None => None,
};
let cookie_jar = match cookie_jar_id {
Some(id) => Some(app_handle.db().get_cookie_jar(id)?),
None => None,
};
let r = match send_http_request(
&window,
&request,
&response,
environment,
cookie_jar,
&mut cancel_rx,
)
.await
{
Ok(r) => r,
Err(e) => {
let resp = app_handle.db().get_http_response(&response.id)?;
app_handle.db().upsert_http_response(
&HttpResponse {
state: HttpResponseState::Closed,
error: Some(e.to_string()),
..resp
},
&UpdateSource::from_window_label(window.label()),
&blobs,
)?
}
};
Ok(r)
}
#[tauri::command]
async fn cmd_install_plugin<R: Runtime>(
directory: &str,
url: Option<String>,
plugin_manager: State<'_, PluginManager>,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> YaakResult<Plugin> {
let plugin = app_handle.db().upsert_plugin(
&Plugin { directory: directory.into(), url, enabled: true, ..Default::default() },
&UpdateSource::from_window_label(window.label()),
)?;
plugin_manager
.add_plugin(
&PluginContext::new(Some(window.label().to_string()), window.workspace_id()),
&plugin,
)
.await?;
Ok(plugin)
}
#[tauri::command]
async fn cmd_create_grpc_request<R: Runtime>(
workspace_id: &str,
name: &str,
sort_priority: f64,
folder_id: Option<&str>,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> YaakResult<GrpcRequest> {
Ok(app_handle.db().upsert_grpc_request(
&GrpcRequest {
workspace_id: workspace_id.to_string(),
name: name.to_string(),
folder_id: folder_id.map(|s| s.to_string()),
sort_priority,
..Default::default()
},
&UpdateSource::from_window_label(window.label()),
)?)
}
#[tauri::command]
async fn cmd_reload_plugins<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<()> {
let plugins = app_handle.db().list_plugins()?;
let plugin_context =
PluginContext::new(Some(window.label().to_string()), window.workspace_id());
let _errors = plugin_manager.initialize_all_plugins(plugins, &plugin_context).await;
// Note: errors are returned but we don't show toasts here since this is a manual reload
Ok(())
}
#[tauri::command]
async fn cmd_plugin_info<R: Runtime>(
id: &str,
app_handle: AppHandle<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<PluginMetadata> {
let plugin = app_handle.db().get_plugin(id)?;
Ok(plugin_manager
.get_plugin_by_dir(plugin.directory.as_str())
.await
.ok_or(GenericError("Failed to find plugin for info".to_string()))?
.info())
}
#[tauri::command]
async fn cmd_delete_all_grpc_connections<R: Runtime>(
request_id: &str,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> YaakResult<()> {
Ok(app_handle.db().delete_all_grpc_connections_for_request(
request_id,
&UpdateSource::from_window_label(window.label()),
)?)
}
#[tauri::command]
async fn cmd_delete_send_history<R: Runtime>(
workspace_id: &str,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> YaakResult<()> {
Ok(app_handle.with_tx(|tx| {
let source = &UpdateSource::from_window_label(window.label());
tx.delete_all_http_responses_for_workspace(workspace_id, source)?;
tx.delete_all_grpc_connections_for_workspace(workspace_id, source)?;
tx.delete_all_websocket_connections_for_workspace(workspace_id, source)?;
Ok(())
})?)
}
#[tauri::command]
async fn cmd_delete_all_http_responses<R: Runtime>(
request_id: &str,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> YaakResult<()> {
Ok(app_handle.db().delete_all_http_responses_for_request(
request_id,
&UpdateSource::from_window_label(window.label()),
)?)
}
#[tauri::command]
async fn cmd_get_workspace_meta<R: Runtime>(
app_handle: AppHandle<R>,
workspace_id: &str,
) -> YaakResult<WorkspaceMeta> {
let db = app_handle.db();
let workspace = db.get_workspace(workspace_id)?;
Ok(db.get_or_create_workspace_meta(&workspace.id)?)
}
#[tauri::command]
async fn cmd_new_child_window(
parent_window: WebviewWindow,
url: &str,
label: &str,
title: &str,
inner_size: (f64, f64),
) -> YaakResult<()> {
window::create_child_window(&parent_window, url, label, title, inner_size)?;
Ok(())
}
#[tauri::command]
async fn cmd_new_main_window(app_handle: AppHandle, url: &str) -> YaakResult<()> {
window::create_main_window(&app_handle, url)?;
Ok(())
}
#[tauri::command]
async fn cmd_check_for_updates<R: Runtime>(
window: WebviewWindow<R>,
yaak_updater: State<'_, Mutex<YaakUpdater>>,
) -> YaakResult<bool> {
let update_mode = get_update_mode(&window).await?;
let settings = window.db().get_settings();
Ok(yaak_updater
.lock()
.await
.check_now(&window, update_mode, settings.auto_download_updates, UpdateTrigger::User)
.await?)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let mut builder = tauri::Builder::default().plugin(
Builder::default()
.targets([
Target::new(TargetKind::Stdout),
Target::new(TargetKind::LogDir { file_name: None }),
Target::new(TargetKind::Webview),
])
.level_for("plugin_runtime", log::LevelFilter::Info)
.level_for("cookie_store", log::LevelFilter::Info)
.level_for("eventsource_client::event_parser", log::LevelFilter::Info)
.level_for("h2", log::LevelFilter::Info)
.level_for("hyper", log::LevelFilter::Info)
.level_for("hyper_util", log::LevelFilter::Info)
.level_for("hyper_rustls", log::LevelFilter::Info)
.level_for("reqwest", log::LevelFilter::Info)
.level_for("sqlx", log::LevelFilter::Debug)
.level_for("tao", log::LevelFilter::Info)
.level_for("tokio_util", log::LevelFilter::Info)
.level_for("tonic", log::LevelFilter::Info)
.level_for("tower", log::LevelFilter::Info)
.level_for("tracing", log::LevelFilter::Warn)
.level_for("swc_ecma_codegen", log::LevelFilter::Off)
.level_for("swc_ecma_transforms_base", log::LevelFilter::Off)
.with_colors(ColoredLevelConfig::default())
.level(if is_dev() { log::LevelFilter::Debug } else { log::LevelFilter::Info })
.build(),
);
// Only enable single-instance in production builds. In dev mode, we want to allow
// multiple instances for testing and worktree workflows (running multiple branches).
if !is_dev() {
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
// When trying to open a new app instance (common operation on Linux),
// focus the first existing window we find instead of opening a new one
// TODO: Keep track of the last focused window and always focus that one
if let Some(window) = app.webview_windows().values().next() {
let _ = window.set_focus();
}
}));
}
builder = builder
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_opener::init())
// Don't restore StateFlags::DECORATIONS because we want to be able to toggle them on/off on a restart
// We could* make this work if we toggled them in the frontend before the window closes, but, this is nicer.
.plugin(
tauri_plugin_window_state::Builder::new()
.with_state_flags(StateFlags::all() - StateFlags::DECORATIONS)
.build(),
)
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_fs::init())
.plugin(yaak_mac_window::init())
.plugin(models_ext::init()) // Database setup only. Must be before plugins_ext which depends on db
.plugin(plugins_ext::init())
.plugin(yaak_fonts::init());
#[cfg(feature = "license")]
{
builder = builder.plugin(yaak_license::init());
}
#[cfg(feature = "updater")]
{
builder = builder.plugin(tauri_plugin_updater::Builder::default().build());
}
builder
.setup(|app| {
// Initialize HTTP connection manager
app.manage(yaak_http::manager::HttpConnectionManager::new());
// Initialize encryption manager
let query_manager =
app.state::<yaak_models::query_manager::QueryManager>().inner().clone();
let app_id = app.config().identifier.to_string();
app.manage(yaak_crypto::manager::EncryptionManager::new(query_manager, app_id));
{
let app_handle = app.app_handle().clone();
app.deep_link().on_open_url(move |event| {
info!("Handling deep link open");
let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
for url in event.urls() {
if let Err(e) = handle_deep_link(&app_handle, &url).await {
warn!("Failed to handle deep link {}: {e:?}", url.to_string());
let _ = app_handle.emit(
"show_toast",
ShowToastRequest {
message: format!(
"Error handling deep link: {}",
e.to_string()
),
color: Some(Color::Danger),
icon: None,
timeout: None,
},
);
};
}
});
});
};
// Add updater
let yaak_updater = YaakUpdater::new();
app.manage(Mutex::new(yaak_updater));
// Add notifier
let yaak_notifier = YaakNotifier::new();
app.manage(Mutex::new(yaak_notifier));
// Add GRPC manager
let protoc_include_dir = app
.path()
.resolve("vendored/protoc/include", BaseDirectory::Resource)
.expect("failed to resolve protoc include directory");
let protoc_bin_name = if cfg!(windows) { "yaakprotoc.exe" } else { "yaakprotoc" };
let protoc_bin_path = app
.path()
.resolve(format!("vendored/protoc/{}", protoc_bin_name), BaseDirectory::Resource)
.expect("failed to resolve yaakprotoc binary");
let grpc_config = GrpcConfig { protoc_include_dir, protoc_bin_path };
let grpc_handle = GrpcHandle::new(grpc_config);
app.manage(Mutex::new(grpc_handle));
// Add WebSocket manager
let ws_manager = yaak_ws::WebsocketManager::new();
app.manage(Mutex::new(ws_manager));
// Specific settings
let settings = app.db().get_settings();
app.app_handle().set_native_titlebar(settings.use_native_titlebar);
monitor_plugin_events(&app.app_handle().clone());
Ok(())
})
.invoke_handler(tauri::generate_handler![
cmd_call_http_authentication_action,
cmd_call_http_request_action,
cmd_call_websocket_request_action,
cmd_call_workspace_action,
cmd_call_folder_action,
cmd_call_grpc_request_action,
cmd_check_for_updates,
cmd_create_grpc_request,
cmd_curl_to_request,
cmd_delete_all_grpc_connections,
cmd_delete_all_http_responses,
cmd_delete_send_history,
cmd_dismiss_notification,
cmd_export_data,
cmd_http_request_body,
cmd_http_response_body,
cmd_format_json,
cmd_get_http_authentication_summaries,
cmd_get_http_authentication_config,
cmd_get_sse_events,
cmd_get_http_response_events,
cmd_get_workspace_meta,
cmd_grpc_go,
cmd_grpc_reflect,
cmd_grpc_request_actions,
cmd_http_request_actions,
cmd_websocket_request_actions,
cmd_workspace_actions,
cmd_folder_actions,
cmd_import_data,
cmd_install_plugin,
cmd_metadata,
cmd_new_child_window,
cmd_new_main_window,
cmd_plugin_info,
cmd_reload_plugins,
cmd_render_template,
cmd_restart,
cmd_save_response,
cmd_send_ephemeral_request,
cmd_send_http_request,
cmd_send_folder,
cmd_template_function_config,
cmd_template_function_summaries,
cmd_template_tokens_to_string,
//
//
// Migrated commands
crate::commands::cmd_decrypt_template,
crate::commands::cmd_enable_encryption,
crate::commands::cmd_get_themes,
crate::commands::cmd_reveal_workspace_key,
crate::commands::cmd_secure_template,
crate::commands::cmd_set_workspace_key,
crate::commands::cmd_show_workspace_key,
//
// Models commands
models_ext::models_delete,
models_ext::models_duplicate,
models_ext::models_get_graphql_introspection,
models_ext::models_get_settings,
models_ext::models_grpc_events,
models_ext::models_upsert,
models_ext::models_upsert_graphql_introspection,
models_ext::models_websocket_events,
models_ext::models_workspace_models,
//
// Sync commands
sync_ext::cmd_sync_calculate,
sync_ext::cmd_sync_calculate_fs,
sync_ext::cmd_sync_apply,
sync_ext::cmd_sync_watch,
//
// Git commands
git_ext::cmd_git_checkout,
git_ext::cmd_git_branch,
git_ext::cmd_git_delete_branch,
git_ext::cmd_git_merge_branch,
git_ext::cmd_git_status,
git_ext::cmd_git_log,
git_ext::cmd_git_initialize,
git_ext::cmd_git_commit,
git_ext::cmd_git_fetch_all,
git_ext::cmd_git_push,
git_ext::cmd_git_pull,
git_ext::cmd_git_add,
git_ext::cmd_git_unstage,
git_ext::cmd_git_add_credential,
git_ext::cmd_git_remotes,
git_ext::cmd_git_add_remote,
git_ext::cmd_git_rm_remote,
//
// WebSocket commands
ws_ext::cmd_ws_upsert_request,
ws_ext::cmd_ws_duplicate_request,
ws_ext::cmd_ws_delete_request,
ws_ext::cmd_ws_delete_connection,
ws_ext::cmd_ws_delete_connections,
ws_ext::cmd_ws_list_events,
ws_ext::cmd_ws_list_requests,
ws_ext::cmd_ws_list_connections,
ws_ext::cmd_ws_send,
ws_ext::cmd_ws_close,
ws_ext::cmd_ws_connect,
])
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(|app_handle, event| {
match event {
RunEvent::Ready => {
let _ = window::create_main_window(app_handle, "/");
let h = app_handle.clone();
tauri::async_runtime::spawn(async move {
let info = history::get_or_upsert_launch_info(&h);
debug!("Launched Yaak {:?}", info);
});
// Cancel pending requests
let h = app_handle.clone();
tauri::async_runtime::block_on(async move {
let db = h.db();
let _ = db.cancel_pending_http_responses();
let _ = db.cancel_pending_grpc_connections();
let _ = db.cancel_pending_websocket_connections();
});
}
RunEvent::WindowEvent { event: WindowEvent::Focused(true), label, .. } => {
if cfg!(feature = "updater") {
// Run update check whenever the window is focused
let w = app_handle.get_webview_window(&label).unwrap();
let h = app_handle.clone();
tauri::async_runtime::spawn(async move {
let settings = w.db().get_settings();
if settings.autoupdate {
time::sleep(Duration::from_secs(3)).await; // Wait a bit so it's not so jarring
let val: State<'_, Mutex<YaakUpdater>> = h.state();
let update_mode = get_update_mode(&w).await.unwrap();
if let Err(e) = val
.lock()
.await
.maybe_check(&w, settings.auto_download_updates, update_mode)
.await
{
warn!("Failed to check for updates {e:?}");
}
};
});
}
let h = app_handle.clone();
tauri::async_runtime::spawn(async move {
let windows = h.webview_windows();
let w = windows.values().next().unwrap();
tokio::time::sleep(Duration::from_millis(4000)).await;
let val: State<'_, Mutex<YaakNotifier>> = w.state();
let mut n = val.lock().await;
if let Err(e) = n.maybe_check(&w).await {
warn!("Failed to check for notifications {}", e)
}
});
}
RunEvent::WindowEvent { event: WindowEvent::CloseRequested { .. }, .. } => {
if let Err(e) = app_handle.save_window_state(StateFlags::all()) {
warn!("Failed to save window state {e:?}");
} else {
info!("Saved window state");
};
}
_ => {}
};
});
}
async fn get_update_mode<R: Runtime>(window: &WebviewWindow<R>) -> YaakResult<UpdateMode> {
let settings = window.db().get_settings();
Ok(UpdateMode::new(settings.update_channel.as_str()))
}
fn safe_uri(endpoint: &str) -> String {
if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
endpoint.into()
} else {
format!("http://{}", endpoint)
}
}
fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
let (rx_id, mut rx) = plugin_manager.subscribe("app").await;
while let Some(event) = rx.recv().await {
let app_handle = app_handle.clone();
let plugin =
match plugin_manager.get_plugin_by_ref_id(event.plugin_ref_id.as_str()).await {
None => {
warn!("Failed to get plugin for event {:?}", event);
continue;
}
Some(p) => p,
};
// We might have recursive back-and-forth calls between app and plugin, so we don't
// want to block here
tauri::async_runtime::spawn(async move {
let ev = plugin_events::handle_plugin_event(&app_handle, &event, &plugin).await;
let ev = match ev {
Ok(Some(ev)) => ev,
Ok(None) => return,
Err(e) => {
warn!("Failed to handle plugin event: {e:?}");
let _ = app_handle.emit(
"show_toast",
InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: e.to_string(),
color: Some(Color::Danger),
icon: None,
timeout: Some(30000),
}),
);
return;
}
};
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &ev).await {
warn!("Failed to reply to plugin manager: {:?}", e)
}
});
}
plugin_manager.unsubscribe(rx_id.as_str()).await;
});
}
async fn call_frontend<R: Runtime>(
window: &WebviewWindow<R>,
event: &InternalEvent,
) -> Option<InternalEventPayload> {
window.emit_to(window.label(), "plugin_event", event.clone()).unwrap();
let (tx, mut rx) = tokio::sync::watch::channel(None);
let reply_id = event.id.clone();
let event_id = window.clone().listen(reply_id, move |ev| {
let resp: InternalEvent = serde_json::from_str(ev.payload()).unwrap();
if let Err(e) = tx.send(Some(resp.payload)) {
warn!("Failed to prompt for text {e:?}");
}
});
// When reply shows up, unlisten to events and return
if let Err(e) = rx.changed().await {
warn!("Failed to check channel changed {e:?}");
}
window.unlisten(event_id);
let v = rx.borrow();
v.to_owned()
}
fn get_window_from_plugin_context<R: Runtime>(
app_handle: &AppHandle<R>,
plugin_context: &PluginContext,
) -> Result<WebviewWindow<R>> {
let label = match &plugin_context.label {
Some(label) => label,
None => {
return app_handle
.webview_windows()
.iter()
.next()
.map(|(_, w)| w.to_owned())
.ok_or(GenericError("No windows open".to_string()));
}
};
let window = app_handle
.webview_windows()
.iter()
.find_map(|(_, w)| if w.label() == label { Some(w.to_owned()) } else { None });
if window.is_none() {
error!("Failed to find window by {plugin_context:?}");
}
Ok(window.ok_or(GenericError(format!("Failed to find window for {}", label)))?)
}
fn workspace_from_window<R: Runtime>(window: &WebviewWindow<R>) -> Option<Workspace> {
window.workspace_id().and_then(|id| window.db().get_workspace(&id).ok())
}
fn environment_from_window<R: Runtime>(window: &WebviewWindow<R>) -> Option<Environment> {
window.environment_id().and_then(|id| window.db().get_environment(&id).ok())
}
fn cookie_jar_from_window<R: Runtime>(window: &WebviewWindow<R>) -> Option<CookieJar> {
window.cookie_jar_id().and_then(|id| window.db().get_cookie_jar(&id).ok())
}