mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Show errors on failure (#5643)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -9,7 +9,7 @@ use crate::commands::configure::handle_configure;
|
||||
use crate::commands::info::handle_info;
|
||||
use crate::commands::project::{handle_project_default, handle_projects_interactive};
|
||||
use crate::commands::recipe::{handle_deeplink, handle_list, handle_open, handle_validate};
|
||||
// Import the new handlers from commands::schedule
|
||||
|
||||
use crate::commands::schedule::{
|
||||
handle_schedule_add, handle_schedule_cron_help, handle_schedule_list, handle_schedule_remove,
|
||||
handle_schedule_run_now, handle_schedule_services_status, handle_schedule_services_stop,
|
||||
|
||||
@@ -2,43 +2,73 @@ use anyhow::Result;
|
||||
use console::style;
|
||||
use goose::config::paths::Paths;
|
||||
use goose::config::Config;
|
||||
use goose::session::session_manager::{DB_NAME, SESSIONS_FOLDER};
|
||||
use serde_yaml;
|
||||
|
||||
fn print_aligned(label: &str, value: &str, width: usize) {
|
||||
println!(" {:<width$} {}", label, value, width = width);
|
||||
}
|
||||
|
||||
use goose::config::base::CONFIG_YAML_NAME;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
fn check_path_status(path: &Path) -> String {
|
||||
if path.exists() {
|
||||
"".to_string()
|
||||
} else {
|
||||
let mut current = path.parent();
|
||||
while let Some(parent) = current {
|
||||
if parent.exists() {
|
||||
return match fs::metadata(parent).map(|m| !m.permissions().readonly()) {
|
||||
Ok(true) => style("missing (can create)").dim().to_string(),
|
||||
Ok(false) => style("missing (read-only parent)").red().to_string(),
|
||||
Err(_) => style("missing (cannot check)").red().to_string(),
|
||||
};
|
||||
}
|
||||
current = parent.parent();
|
||||
}
|
||||
style("missing (no writable parent)").red().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_info(verbose: bool) -> Result<()> {
|
||||
let logs_dir = Paths::in_state_dir("logs");
|
||||
let sessions_dir = Paths::in_data_dir("sessions");
|
||||
let sessions_db = sessions_dir.join("sessions.db");
|
||||
|
||||
// Get paths using a stored reference to the global config
|
||||
let sessions_dir = Paths::in_data_dir(SESSIONS_FOLDER);
|
||||
let sessions_db = sessions_dir.join(DB_NAME);
|
||||
let config = Config::global();
|
||||
let config_dir = Paths::config_dir().display().to_string();
|
||||
let config_dir = Paths::config_dir();
|
||||
let config_yaml_file = config_dir.join(CONFIG_YAML_NAME);
|
||||
|
||||
// Define the labels and their corresponding path values once.
|
||||
let paths = [
|
||||
("Config dir:", config_dir),
|
||||
("Sessions DB (sqlite):", sessions_db.display().to_string()),
|
||||
("Logs dir:", logs_dir.display().to_string()),
|
||||
("Config dir:", &config_dir),
|
||||
("Config yaml:", &config_yaml_file),
|
||||
("Sessions DB (sqlite):", &sessions_db),
|
||||
("Logs dir:", &logs_dir),
|
||||
];
|
||||
|
||||
// Calculate padding: use the max length of the label plus extra space.
|
||||
let basic_padding = paths.iter().map(|(l, _)| l.len()).max().unwrap_or(0) + 4;
|
||||
let label_padding = paths.iter().map(|(l, _)| l.len()).max().unwrap_or(0) + 4;
|
||||
let path_padding = paths
|
||||
.iter()
|
||||
.map(|(_, p)| p.display().to_string().len())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 4;
|
||||
|
||||
// Print version information
|
||||
println!("{}", style("goose Version:").cyan().bold());
|
||||
print_aligned("Version:", env!("CARGO_PKG_VERSION"), basic_padding);
|
||||
print_aligned("Version:", env!("CARGO_PKG_VERSION"), label_padding);
|
||||
println!();
|
||||
|
||||
// Print location information
|
||||
println!("{}", style("goose Locations:").cyan().bold());
|
||||
println!("{}", style("Paths:").cyan().bold());
|
||||
for (label, path) in &paths {
|
||||
print_aligned(label, path, basic_padding);
|
||||
println!(
|
||||
"{:<label_padding$}{:<path_padding$}{}",
|
||||
label,
|
||||
path.display(),
|
||||
check_path_status(path)
|
||||
);
|
||||
}
|
||||
|
||||
// Print verbose info if requested
|
||||
if verbose {
|
||||
println!("\n{}", style("goose Configuration:").cyan().bold());
|
||||
let values = config.all_values()?;
|
||||
|
||||
@@ -4,7 +4,7 @@ use goose_cli::cli::cli;
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
if let Err(e) = goose_cli::logging::setup_logging(None, None) {
|
||||
eprintln!("Warning: Failed to initialize telemetry: {}", e);
|
||||
eprintln!("Warning: Failed to initialize logging: {}", e);
|
||||
}
|
||||
|
||||
let result = cli().await;
|
||||
|
||||
@@ -28,13 +28,10 @@ async fn shutdown_signal() {
|
||||
}
|
||||
|
||||
pub async fn run() -> Result<()> {
|
||||
// Initialize logging and telemetry
|
||||
crate::logging::setup_logging(Some("goosed"))?;
|
||||
|
||||
let settings = configuration::Settings::new()?;
|
||||
|
||||
// Initialize pricing cache on startup
|
||||
tracing::info!("Initializing pricing cache...");
|
||||
if let Err(e) = initialize_pricing_cache().await {
|
||||
tracing::warn!(
|
||||
"Failed to initialize pricing cache: {}. Pricing data may not be available.",
|
||||
@@ -61,7 +58,7 @@ pub async fn run() -> Result<()> {
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(settings.socket_addr()).await?;
|
||||
info!("listening on {}", listener.local_addr()?);
|
||||
// Ensure the listener/socket is properly closed on cancellation by using graceful shutdown
|
||||
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await?;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
use tracing_appender::rolling::Rotation;
|
||||
use tracing_subscriber::{
|
||||
filter::LevelFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
|
||||
@@ -20,11 +20,8 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> {
|
||||
} else {
|
||||
format!("{}.log", timestamp)
|
||||
};
|
||||
let file_appender = tracing_appender::rolling::RollingFileAppender::new(
|
||||
Rotation::NEVER, // we do manual rotation via file naming and cleanup_old_logs
|
||||
log_dir,
|
||||
log_filename,
|
||||
);
|
||||
let file_appender =
|
||||
tracing_appender::rolling::RollingFileAppender::new(Rotation::NEVER, log_dir, log_filename);
|
||||
|
||||
// Create JSON file logging layer
|
||||
let file_layer = fmt::layer()
|
||||
@@ -34,35 +31,27 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> {
|
||||
.with_ansi(false)
|
||||
.with_file(true);
|
||||
|
||||
// Create console logging layer for development - INFO and above only
|
||||
let base_env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
EnvFilter::new("")
|
||||
.add_directive("mcp_client=info".parse().unwrap())
|
||||
.add_directive("goose=debug".parse().unwrap())
|
||||
.add_directive("goose_server=info".parse().unwrap())
|
||||
.add_directive("tower_http=info".parse().unwrap())
|
||||
.add_directive(LevelFilter::WARN.into())
|
||||
});
|
||||
|
||||
let console_layer = fmt::layer()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_target(true)
|
||||
.with_level(true)
|
||||
.with_ansi(true)
|
||||
.with_file(true)
|
||||
.with_ansi(false)
|
||||
.with_line_number(true)
|
||||
.pretty();
|
||||
|
||||
// Base filter for all logging
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
// Set default levels for different modules
|
||||
EnvFilter::new("")
|
||||
// Set mcp-client to DEBUG
|
||||
.add_directive("mcp_client=debug".parse().unwrap())
|
||||
// Set goose module to DEBUG
|
||||
.add_directive("goose=debug".parse().unwrap())
|
||||
// Set goose-server to INFO
|
||||
.add_directive("goose_server=info".parse().unwrap())
|
||||
// Set tower-http to INFO for request logging
|
||||
.add_directive("tower_http=info".parse().unwrap())
|
||||
// Set everything else to WARN
|
||||
.add_directive(LevelFilter::WARN.into())
|
||||
});
|
||||
|
||||
let mut layers = vec![
|
||||
file_layer.with_filter(env_filter).boxed(),
|
||||
console_layer.with_filter(LevelFilter::INFO).boxed(),
|
||||
file_layer.with_filter(base_env_filter.clone()).boxed(),
|
||||
console_layer.with_filter(base_env_filter).boxed(),
|
||||
];
|
||||
|
||||
if let Ok((otlp_tracing_layer, otlp_metrics_layer, otlp_logs_layer)) = otlp_layer::init_otlp() {
|
||||
@@ -89,9 +78,7 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> {
|
||||
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
subscriber
|
||||
.try_init()
|
||||
.context("Failed to set global subscriber")?;
|
||||
subscriber.try_init()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use thiserror::Error;
|
||||
|
||||
const KEYRING_SERVICE: &str = "goose";
|
||||
const KEYRING_USERNAME: &str = "secrets";
|
||||
pub const CONFIG_YAML_NAME: &str = "config.yaml";
|
||||
|
||||
#[cfg(test)]
|
||||
const TEST_KEYRING_SERVICE: &str = "goose-test";
|
||||
@@ -119,9 +120,7 @@ impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let config_dir = Paths::config_dir();
|
||||
|
||||
std::fs::create_dir_all(&config_dir).expect("Failed to create config directory");
|
||||
|
||||
let config_path = config_dir.join("config.yaml");
|
||||
let config_path = config_dir.join(CONFIG_YAML_NAME);
|
||||
|
||||
let secrets = match env::var("GOOSE_DISABLE_KEYRING") {
|
||||
Ok(_) => SecretStorage::File {
|
||||
@@ -433,7 +432,6 @@ impl Config {
|
||||
// Convert to YAML for storage
|
||||
let yaml_value = serde_yaml::to_string(&values)?;
|
||||
|
||||
// Ensure the directory exists
|
||||
if let Some(parent) = self.config_path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| ConfigError::DirectoryError(e.to_string()))?;
|
||||
|
||||
@@ -24,7 +24,8 @@ pub fn prepare_log_directory(component: &str, use_date_subdir: bool) -> Result<P
|
||||
component_dir
|
||||
};
|
||||
|
||||
fs::create_dir_all(&log_dir).context("Failed to create log directory")?;
|
||||
fs::create_dir_all(&log_dir)
|
||||
.with_context(|| format!("Failed to create log directory: {:?}", log_dir))?;
|
||||
|
||||
Ok(log_dir)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -166,7 +166,6 @@ impl PricingCache {
|
||||
|
||||
self.save_to_disk(&cached_data).await?;
|
||||
|
||||
// Update memory cache
|
||||
{
|
||||
let mut cache = self.memory_cache.write().await;
|
||||
*cache = Some(cached_data);
|
||||
@@ -179,15 +178,6 @@ impl PricingCache {
|
||||
pub async fn initialize(&self) -> Result<()> {
|
||||
// Try loading from disk first
|
||||
if let Ok(Some(cached)) = self.load_from_disk().await {
|
||||
// Log how many models we have cached
|
||||
let total_models: usize = cached.pricing.values().map(|models| models.len()).sum();
|
||||
tracing::debug!(
|
||||
"Loaded {} providers with {} total models from disk cache",
|
||||
cached.pricing.len(),
|
||||
total_models
|
||||
);
|
||||
|
||||
// Update memory cache
|
||||
{
|
||||
let mut cache = self.memory_cache.write().await;
|
||||
*cache = Some(cached);
|
||||
@@ -196,8 +186,6 @@ impl PricingCache {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If no disk cache, fetch from OpenRouter
|
||||
tracing::info!("Fetching pricing data from OpenRouter API");
|
||||
self.refresh().await
|
||||
}
|
||||
}
|
||||
@@ -213,14 +201,13 @@ lazy_static::lazy_static! {
|
||||
static ref PRICING_CACHE: PricingCache = PricingCache::new();
|
||||
}
|
||||
|
||||
/// Create a properly configured HTTP client for the current runtime
|
||||
fn create_http_client() -> Client {
|
||||
fn create_http_client() -> Result<Client> {
|
||||
Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.pool_idle_timeout(Duration::from_secs(90))
|
||||
.pool_max_idle_per_host(10)
|
||||
.build()
|
||||
.expect("Failed to create HTTP client")
|
||||
.map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
/// OpenRouter model pricing information
|
||||
@@ -254,7 +241,7 @@ pub struct OpenRouterModelsResponse {
|
||||
|
||||
/// Internal function to fetch pricing data
|
||||
async fn fetch_openrouter_pricing_internal() -> Result<HashMap<String, OpenRouterModel>> {
|
||||
let client = create_http_client();
|
||||
let client = create_http_client()?;
|
||||
let response = client
|
||||
.get("https://openrouter.ai/api/v1/models")
|
||||
.send()
|
||||
|
||||
@@ -190,13 +190,6 @@ impl Scheduler {
|
||||
|
||||
let local_tz = Local::now().timezone();
|
||||
|
||||
tracing::info!(
|
||||
"Creating cron task for job '{}' cron: '{}' in timezone: {:?}",
|
||||
job.id,
|
||||
cron,
|
||||
local_tz
|
||||
);
|
||||
|
||||
Job::new_async_tz(&cron, local_tz, move |_uuid, _l| {
|
||||
tracing::info!("Cron task triggered for job '{}'", job_for_task.id);
|
||||
let task_job_id = job_for_task.id.clone();
|
||||
@@ -215,7 +208,6 @@ impl Scheduler {
|
||||
};
|
||||
|
||||
if !should_execute {
|
||||
tracing::info!("Skipping paused job '{}'", task_job_id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -731,15 +723,11 @@ async fn execute_job(
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = SessionManager::update_session(&session.id)
|
||||
SessionManager::update_session(&session.id)
|
||||
.schedule_id(Some(job.id.clone()))
|
||||
.recipe(Some(recipe))
|
||||
.apply()
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to update session: {}", e);
|
||||
}
|
||||
|
||||
.await?;
|
||||
Ok(session.id)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ use tracing::{info, warn};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
const CURRENT_SCHEMA_VERSION: i32 = 5;
|
||||
pub const SESSIONS_FOLDER: &str = "sessions";
|
||||
pub const DB_NAME: &str = "sessions.db";
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -335,7 +337,7 @@ pub struct SessionStorage {
|
||||
}
|
||||
|
||||
pub fn ensure_session_dir() -> Result<PathBuf> {
|
||||
let session_dir = Paths::data_dir().join("sessions");
|
||||
let session_dir = Paths::data_dir().join(SESSIONS_FOLDER);
|
||||
|
||||
if !session_dir.exists() {
|
||||
fs::create_dir_all(&session_dir)?;
|
||||
@@ -439,7 +441,7 @@ impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session {
|
||||
impl SessionStorage {
|
||||
async fn new() -> Result<Self> {
|
||||
let session_dir = ensure_session_dir()?;
|
||||
let db_path = session_dir.join("sessions.db");
|
||||
let db_path = session_dir.join(DB_NAME);
|
||||
|
||||
let storage = if db_path.exists() {
|
||||
Self::open(&db_path).await?
|
||||
|
||||
@@ -34,7 +34,6 @@ impl Default for OtlpConfig {
|
||||
|
||||
impl OtlpConfig {
|
||||
pub fn from_config() -> Option<Self> {
|
||||
// Try to get from goose config system (which checks env vars first, then config file)
|
||||
let config = crate::config::Config::global();
|
||||
|
||||
// Try to get the endpoint from config (checks OTEL_EXPORTER_OTLP_ENDPOINT env var first)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ModeSection } from '../mode/ModeSection';
|
||||
import { ToolSelectionStrategySection } from '../tool_selection_strategy/ToolSelectionStrategySection';
|
||||
import SchedulerSection from '../scheduler/SchedulerSection';
|
||||
import DictationSection from '../dictation/DictationSection';
|
||||
import { SecurityToggle } from '../security/SecurityToggle';
|
||||
import { ResponseStylesSection } from '../response_styles/ResponseStylesSection';
|
||||
@@ -48,9 +47,6 @@ export default function ChatSettingsSection() {
|
||||
Choose which scheduling backend to use for scheduled recipes and tasks
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2">
|
||||
<SchedulerSection />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="pb-2 rounded-lg">
|
||||
<CardHeader className="pb-0">
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { SchedulingEngine, Settings } from '../../../utils/settings';
|
||||
|
||||
interface SchedulingEngineOption {
|
||||
key: SchedulingEngine;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const schedulingEngineOptions: SchedulingEngineOption[] = [
|
||||
{
|
||||
key: 'builtin-cron',
|
||||
label: 'Built-in Cron (Default)',
|
||||
description:
|
||||
"Uses Goose's built-in cron scheduler. Simple and reliable for basic scheduling needs.",
|
||||
},
|
||||
{
|
||||
key: 'temporal',
|
||||
label: 'Temporal',
|
||||
description:
|
||||
'Uses Temporal workflow engine for advanced scheduling features. Requires Temporal CLI to be installed.',
|
||||
},
|
||||
];
|
||||
|
||||
interface SchedulerSectionProps {
|
||||
onSchedulingEngineChange?: (engine: SchedulingEngine) => void;
|
||||
}
|
||||
|
||||
export default function SchedulerSection({ onSchedulingEngineChange }: SchedulerSectionProps) {
|
||||
const [schedulingEngine, setSchedulingEngine] = useState<SchedulingEngine>('builtin-cron');
|
||||
|
||||
useEffect(() => {
|
||||
const loadSchedulingEngine = async () => {
|
||||
try {
|
||||
const settings = (await window.electron.getSettings()) as Settings | null;
|
||||
if (settings?.schedulingEngine) {
|
||||
setSchedulingEngine(settings.schedulingEngine);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load scheduling engine setting:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadSchedulingEngine();
|
||||
}, []);
|
||||
|
||||
const handleEngineChange = async (engine: SchedulingEngine) => {
|
||||
try {
|
||||
setSchedulingEngine(engine);
|
||||
|
||||
await window.electron.setSchedulingEngine(engine);
|
||||
|
||||
if (onSchedulingEngineChange) {
|
||||
onSchedulingEngineChange(engine);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save scheduling engine setting:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{schedulingEngineOptions.map((option) => {
|
||||
const isChecked = schedulingEngine === option.key;
|
||||
|
||||
return (
|
||||
<div key={option.key} className="group hover:cursor-pointer text-sm">
|
||||
<div
|
||||
className={`flex items-center justify-between text-text-default py-2 px-2 ${
|
||||
isChecked
|
||||
? 'bg-background-muted'
|
||||
: 'bg-background-default hover:bg-background-muted'
|
||||
} rounded-lg transition-all`}
|
||||
onClick={() => handleEngineChange(option.key)}
|
||||
>
|
||||
<div className="flex">
|
||||
<div>
|
||||
<h3 className="text-text-default">{option.label}</h3>
|
||||
<p className="text-xs text-text-muted mt-[2px]">{option.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="schedulingEngine"
|
||||
value={option.key}
|
||||
checked={isChecked}
|
||||
onChange={() => handleEngineChange(option.key)}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div
|
||||
className="h-4 w-4 rounded-full border border-border-default
|
||||
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
|
||||
peer-checked:bg-white dark:peer-checked:bg-black
|
||||
transition-all duration-200 ease-in-out group-hover:border-border-default"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="mt-4 p-3 bg-background-subtle rounded-md">
|
||||
<p className="text-xs text-text-muted">
|
||||
<strong>Note:</strong> Changing the scheduling engine will apply to new Goose sessions.
|
||||
You will need to restart Goose for the change to take full effect. <br />
|
||||
The scheduling engines do not share the list of schedules.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+26
-28
@@ -2,7 +2,6 @@ import { spawn, ChildProcess } from 'child_process';
|
||||
import { createServer } from 'net';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { getBinaryPath } from './utils/pathUtils';
|
||||
import log from './utils/logger';
|
||||
import { App } from 'electron';
|
||||
@@ -11,7 +10,6 @@ import { Buffer } from 'node:buffer';
|
||||
import { status } from './api';
|
||||
import { Client } from './api/client';
|
||||
|
||||
// Find an available port to start goosed on
|
||||
export const findAvailablePort = (): Promise<number> => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
const server = createServer();
|
||||
@@ -27,11 +25,20 @@ export const findAvailablePort = (): Promise<number> => {
|
||||
};
|
||||
|
||||
// Check if goosed server is ready by polling the status endpoint
|
||||
export const checkServerStatus = async (client: Client): Promise<boolean> => {
|
||||
export const checkServerStatus = async (client: Client, errorLog: string[]): Promise<boolean> => {
|
||||
const interval = 100; // ms
|
||||
const maxAttempts = 1200; // 120s
|
||||
const maxAttempts = 30; // 3s
|
||||
|
||||
const fatal = (line: string) => {
|
||||
const trimmed = line.trim().toLowerCase();
|
||||
return trimmed.startsWith("thread 'main' panicked at") || trimmed.startsWith('error:');
|
||||
};
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
if (errorLog.some(fatal)) {
|
||||
log.error('Detected fatal error in server logs');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await status({ client, throwOnError: true });
|
||||
return true;
|
||||
@@ -48,7 +55,7 @@ export const checkServerStatus = async (client: Client): Promise<boolean> => {
|
||||
const connectToExternalBackend = async (
|
||||
workingDir: string,
|
||||
port: number = 3000
|
||||
): Promise<[number, string, ChildProcess]> => {
|
||||
): Promise<[number, string, ChildProcess, string[]]> => {
|
||||
log.info(`Using external goosed backend on port ${port}`);
|
||||
|
||||
const mockProcess = {
|
||||
@@ -58,7 +65,7 @@ const connectToExternalBackend = async (
|
||||
},
|
||||
} as ChildProcess;
|
||||
|
||||
return [port, workingDir, mockProcess];
|
||||
return [port, workingDir, mockProcess, []];
|
||||
};
|
||||
|
||||
interface GooseProcessEnv {
|
||||
@@ -76,39 +83,23 @@ interface GooseProcessEnv {
|
||||
export const startGoosed = async (
|
||||
app: App,
|
||||
serverSecret: string,
|
||||
dir: string | null = null,
|
||||
dir: string,
|
||||
env: Partial<GooseProcessEnv> = {}
|
||||
): Promise<[number, string, ChildProcess]> => {
|
||||
const homeDir = os.homedir();
|
||||
): Promise<[number, string, ChildProcess, string[]]> => {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
if (!dir) {
|
||||
dir = homeDir;
|
||||
}
|
||||
|
||||
const homeDir = os.homedir();
|
||||
dir = path.resolve(path.normalize(dir));
|
||||
|
||||
if (process.env.GOOSE_EXTERNAL_BACKEND) {
|
||||
return connectToExternalBackend(dir, 3000);
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = fs.lstatSync(dir);
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
log.warn(`Provided path is not a directory, falling back to home directory`);
|
||||
dir = homeDir;
|
||||
}
|
||||
} catch {
|
||||
log.warn(`Directory does not exist, falling back to home directory`);
|
||||
dir = homeDir;
|
||||
}
|
||||
|
||||
let goosedPath = getBinaryPath(app, 'goosed');
|
||||
|
||||
const resolvedGoosedPath = path.resolve(goosedPath);
|
||||
|
||||
const port = await findAvailablePort();
|
||||
const stderrLines: string[] = [];
|
||||
|
||||
log.info(`Starting goosed from: ${resolvedGoosedPath} on port ${port} in dir ${dir}`);
|
||||
|
||||
@@ -183,7 +174,14 @@ export const startGoosed = async (
|
||||
});
|
||||
|
||||
goosedProcess.stderr?.on('data', (data: Buffer) => {
|
||||
log.error(`goosed stderr for port ${port} and dir ${dir}: ${data.toString()}`);
|
||||
const lines = data
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((l) => l.trim());
|
||||
lines.forEach((line) => {
|
||||
log.error(`goosed stderr for port ${port} and dir ${dir}: ${line}`);
|
||||
stderrLines.push(line);
|
||||
});
|
||||
});
|
||||
|
||||
goosedProcess.on('close', (code: number | null) => {
|
||||
@@ -215,5 +213,5 @@ export const startGoosed = async (
|
||||
});
|
||||
|
||||
log.info(`Goosed server successfully started on port ${port}`);
|
||||
return [port, dir, goosedProcess];
|
||||
return [port, dir, goosedProcess, stderrLines];
|
||||
};
|
||||
|
||||
+20
-58
@@ -33,9 +33,7 @@ import {
|
||||
EnvToggles,
|
||||
loadSettings,
|
||||
saveSettings,
|
||||
SchedulingEngine,
|
||||
updateEnvironmentVariables,
|
||||
updateSchedulingEngineEnvironment,
|
||||
} from './utils/settings';
|
||||
import * as crypto from 'crypto';
|
||||
// import electron from "electron";
|
||||
@@ -502,38 +500,20 @@ const createChat = async (
|
||||
scheduledJobId?: string, // Scheduled job ID if applicable
|
||||
recipeId?: string
|
||||
) => {
|
||||
// Initialize variables for process and configuration
|
||||
let port = 0;
|
||||
let workingDir = '';
|
||||
let goosedProcess: import('child_process').ChildProcess | null = null;
|
||||
updateEnvironmentVariables(envToggles);
|
||||
|
||||
{
|
||||
// Apply current environment settings before creating chat
|
||||
updateEnvironmentVariables(envToggles);
|
||||
const envVars = {
|
||||
GOOSE_PATH_ROOT: process.env.GOOSE_PATH_ROOT,
|
||||
};
|
||||
const [port, workingDir, goosedProcess, errorLog] = await startGoosed(
|
||||
app,
|
||||
SERVER_SECRET,
|
||||
dir || os.homedir(),
|
||||
envVars
|
||||
);
|
||||
|
||||
// Apply scheduling engine setting
|
||||
const settings = loadSettings();
|
||||
updateSchedulingEngineEnvironment(settings.schedulingEngine);
|
||||
|
||||
const envVars = {
|
||||
GOOSE_SCHEDULER_TYPE: process.env.GOOSE_SCHEDULER_TYPE,
|
||||
GOOSE_PATH_ROOT: process.env.GOOSE_PATH_ROOT,
|
||||
};
|
||||
const [newPort, newWorkingDir, newGoosedProcess] = await startGoosed(
|
||||
app,
|
||||
SERVER_SECRET,
|
||||
dir,
|
||||
envVars
|
||||
);
|
||||
port = newPort;
|
||||
workingDir = newWorkingDir;
|
||||
goosedProcess = newGoosedProcess;
|
||||
}
|
||||
|
||||
// Create window config with loading state for recipe deeplinks
|
||||
// Load and manage window state
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 940, // large enough to show the sidebar on launch
|
||||
defaultWidth: 940,
|
||||
defaultHeight: 800,
|
||||
});
|
||||
|
||||
@@ -594,23 +574,21 @@ const createChat = async (
|
||||
);
|
||||
goosedClients.set(mainWindow.id, goosedClient);
|
||||
|
||||
console.log('[Main] Waiting for backend server to be ready...');
|
||||
const serverReady = await checkServerStatus(goosedClient);
|
||||
const serverReady = await checkServerStatus(goosedClient, errorLog);
|
||||
if (!serverReady) {
|
||||
throw new Error('Backend server failed to start in time');
|
||||
dialog.showMessageBoxSync({
|
||||
type: 'error',
|
||||
title: 'Goose Failed to Start',
|
||||
message: 'The backend server failed to start.',
|
||||
detail: errorLog.join('\n'),
|
||||
buttons: ['OK'],
|
||||
});
|
||||
app.quit();
|
||||
}
|
||||
|
||||
// Let windowStateKeeper manage the window
|
||||
mainWindowState.manage(mainWindow);
|
||||
|
||||
// Enable spellcheck / right and ctrl + click on mispelled word
|
||||
//
|
||||
// NOTE: We could use webContents.session.availableSpellCheckerLanguages to include
|
||||
// all languages in the list of spell checked words, but it diminishes the times you
|
||||
// get red squigglies back for mispelled english words. Given the rest of Goose only
|
||||
// renders in english right now, this feels like the correct set of language codes
|
||||
// for the moment.
|
||||
//
|
||||
mainWindow.webContents.session.setSpellCheckerLanguages(['en-US', 'en-GB']);
|
||||
mainWindow.webContents.on('context-menu', (_event, params) => {
|
||||
const menu = new Menu();
|
||||
@@ -1167,22 +1145,6 @@ ipcMain.handle('get-goosed-host-port', async (event) => {
|
||||
return client.getConfig().baseUrl || null;
|
||||
});
|
||||
|
||||
ipcMain.handle('set-scheduling-engine', async (_event, engine: string) => {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
settings.schedulingEngine = engine as SchedulingEngine;
|
||||
saveSettings(settings);
|
||||
|
||||
// Update the environment variable immediately
|
||||
updateSchedulingEngineEnvironment(settings.schedulingEngine);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error setting scheduling engine:', error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle menu bar icon visibility
|
||||
ipcMain.handle('set-menu-bar-icon', async (_event, show: boolean) => {
|
||||
try {
|
||||
|
||||
@@ -80,7 +80,6 @@ type ElectronAPI = {
|
||||
getSettings: () => Promise<unknown | null>;
|
||||
getSecretKey: () => Promise<string>;
|
||||
getGoosedHostPort: () => Promise<string | null>;
|
||||
setSchedulingEngine: (engine: string) => Promise<boolean>;
|
||||
setWakelock: (enable: boolean) => Promise<boolean>;
|
||||
getWakelockState: () => Promise<boolean>;
|
||||
openNotificationsSettings: () => Promise<boolean>;
|
||||
@@ -186,7 +185,6 @@ const electronAPI: ElectronAPI = {
|
||||
getSettings: () => ipcRenderer.invoke('get-settings'),
|
||||
getSecretKey: () => ipcRenderer.invoke('get-secret-key'),
|
||||
getGoosedHostPort: () => ipcRenderer.invoke('get-goosed-host-port'),
|
||||
setSchedulingEngine: (engine: string) => ipcRenderer.invoke('set-scheduling-engine', engine),
|
||||
setWakelock: (enable: boolean) => ipcRenderer.invoke('set-wakelock', enable),
|
||||
getWakelockState: () => ipcRenderer.invoke('get-wakelock-state'),
|
||||
openNotificationsSettings: () => ipcRenderer.invoke('open-notifications-settings'),
|
||||
|
||||
@@ -2,23 +2,18 @@ import { app } from 'electron';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Types
|
||||
export interface EnvToggles {
|
||||
GOOSE_SERVER__MEMORY: boolean;
|
||||
GOOSE_SERVER__COMPUTER_CONTROLLER: boolean;
|
||||
}
|
||||
|
||||
export type SchedulingEngine = 'builtin-cron' | 'temporal';
|
||||
|
||||
export interface Settings {
|
||||
envToggles: EnvToggles;
|
||||
showMenuBarIcon: boolean;
|
||||
showDockIcon: boolean;
|
||||
schedulingEngine: SchedulingEngine;
|
||||
enableWakelock: boolean;
|
||||
}
|
||||
|
||||
// Constants
|
||||
const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json');
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
@@ -28,7 +23,6 @@ const defaultSettings: Settings = {
|
||||
},
|
||||
showMenuBarIcon: true,
|
||||
showDockIcon: true,
|
||||
schedulingEngine: 'builtin-cron',
|
||||
enableWakelock: false,
|
||||
};
|
||||
|
||||
@@ -53,7 +47,6 @@ export function saveSettings(settings: Settings): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Environment management
|
||||
export function updateEnvironmentVariables(envToggles: EnvToggles): void {
|
||||
if (envToggles.GOOSE_SERVER__MEMORY) {
|
||||
process.env.GOOSE_SERVER__MEMORY = 'true';
|
||||
@@ -67,12 +60,3 @@ export function updateEnvironmentVariables(envToggles: EnvToggles): void {
|
||||
delete process.env.GOOSE_SERVER__COMPUTER_CONTROLLER;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSchedulingEngineEnvironment(schedulingEngine: SchedulingEngine): void {
|
||||
// Set GOOSE_SCHEDULER_TYPE based on the scheduling engine setting
|
||||
if (schedulingEngine === 'temporal') {
|
||||
process.env.GOOSE_SCHEDULER_TYPE = 'temporal';
|
||||
} else {
|
||||
process.env.GOOSE_SCHEDULER_TYPE = 'legacy';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user