4 Commits

Author SHA1 Message Date
cd4c613758 improve start-minimized and start-on-login behavior
Previously, when Creddy was configured to start minimized, it would always start minimized, regardless of how it was launched. Really, though, when you use this setting what you probably want is for it to start minimized only when it's being launched automatically, i.e. on login. This update changes its behavior so that it will only start minimized when auto-launching.

Additionally, if Creddy detects on startup that its start-on-login configuration doesn't match the system, it will modify its own settings to match the system (unless it's the very first launch, of course.) That way if you disable Creddy's start-on-login behavior from your system dialog, it will respect your change.
2024-12-30 21:09:45 -05:00
efbf6c687c add test to ensure that client and server agree on socket address 2024-12-28 07:36:38 -05:00
ee495478ff start working on test for server address 2024-12-28 07:24:43 -05:00
4c18de8b7a fix docker credential helper when credentials are not found 2024-12-28 06:59:09 -05:00
15 changed files with 155 additions and 63 deletions

View File

@ -1,6 +1,6 @@
{
"name": "creddy",
"version": "0.6.1",
"version": "0.6.3",
"scripts": {
"dev": "vite",
"build": "vite build",

2
src-tauri/Cargo.lock generated
View File

@ -1217,7 +1217,7 @@ dependencies = [
[[package]]
name = "creddy"
version = "0.6.1"
version = "0.6.3"
dependencies = [
"argon2",
"auto-launch",

View File

@ -1,6 +1,6 @@
[package]
name = "creddy"
version = "0.6.1"
version = "0.6.3"
description = "A friendly AWS credentials manager"
authors = ["Joseph Montanaro"]
license = ""
@ -30,7 +30,7 @@ tauri-build = { version = "2.0.0-beta", features = [] }
[dependencies]
creddy_cli = { path = "./creddy_cli" }
tauri = { version = "2.0.0-beta", features = ["tray-icon"] }
tauri = { version = "2.0.0-beta", features = ["tray-icon", "test"] }
sodiumoxide = "0.2.7"
sysinfo = "0.26.8"
aws-config = "1.5.3"

View File

@ -29,11 +29,20 @@ pub fn docker_get(global_args: GlobalArgs) -> anyhow::Result<()> {
server_url: server_url.trim().to_owned()
};
match super::make_request(global_args.server_addr, &req)?? {
CliResponse::Credential(CliCredential::Docker(d)) => {
let server_resp = super::make_request(global_args.server_addr, &req)?;
match server_resp {
Ok(CliResponse::Credential(CliCredential::Docker(d))) => {
println!("{}", serde_json::to_string(&d)?);
},
r => bail!("Unexpected response from server: {r}"),
Err(e) if e.code == "NoCredentials" => {
// To indicate credentials are not found, a credential helper *must* print
// this message to stdout, then exit 1. Any other message/status will cause
// some builds to fail. This is, of course, not documented anywhere.
println!("credentials not found in native keychain");
std::process::exit(1);
},
Err(e) => Err(e)?,
Ok(r) => bail!("Unexpected response from server: {r}"),
}
Ok(())
}

View File

@ -65,7 +65,7 @@ pub struct GlobalArgs {
#[derive(Debug, Subcommand)]
pub enum Action {
/// Launch Creddy
Run,
Run(RunArgs),
/// Request credentials from Creddy and output to stdout
Get(GetArgs),
/// Inject credentials into the environment of another command
@ -78,6 +78,14 @@ pub enum Action {
}
#[derive(Debug, Args)]
pub struct RunArgs {
/// Minimize to system tray on launch
#[arg(long, default_value_t = false)]
pub minimized: bool,
}
#[derive(Debug, Args)]
pub struct GetArgs {
/// If unspecified, use default credentials

View File

@ -6,11 +6,11 @@ pub use cli::{
exec,
get,
GlobalArgs,
RunArgs,
invoke_shortcut,
};
pub(crate) use platform::connect;
pub use platform::server_addr;
pub use platform::{connect, server_addr};
pub mod proto;
@ -27,8 +27,7 @@ mod platform {
use tokio::net::UnixStream;
pub async fn connect(addr: Option<PathBuf>) -> Result<UnixStream, std::io::Error> {
let default = if cfg!(debug_assertions) { "creddy-server-dev" } else { "creddy-server" };
let path = addr.unwrap_or_else(|| server_addr(default));
let path = addr.unwrap_or_else(|| server_addr("creddy-server"));
UnixStream::connect(&path).await
}

View File

@ -1,13 +1,17 @@
use std::env;
use std::process::{self, Command};
use creddy_cli::{Action, Cli};
use creddy_cli::{
Action,
Cli,
RunArgs,
};
fn main() {
let cli = Cli::parse();
let res = match cli.action {
None | Some(Action::Run)=> launch_gui(),
None => launch_gui(RunArgs { minimized: false }),
Some(Action::Run(run_args)) => launch_gui(run_args),
Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args),
Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args),
Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args),
@ -21,7 +25,7 @@ fn main() {
}
fn launch_gui() -> anyhow::Result<()> {
fn launch_gui(run_args: RunArgs) -> anyhow::Result<()> {
let mut path = env::current_exe()?;
path.pop(); // bin dir
@ -31,6 +35,10 @@ fn launch_gui() -> anyhow::Result<()> {
path.push("creddy.exe"); // exe in main install dir (aka gui exe)
Command::new(path).spawn()?;
let mut cmd = Command::new(path);
if run_args.minimized {
cmd.arg("--minimized");
}
cmd.spawn()?;
Ok(())
}

View File

@ -99,8 +99,8 @@ pub struct DockerCredential {
#[derive(Debug, Serialize, Deserialize)]
pub struct ServerError {
code: String,
msg: String,
pub code: String,
pub msg: String,
}
impl Display for ServerError {

View File

@ -15,7 +15,7 @@ use tauri::{
RunEvent,
WindowEvent,
};
use creddy_cli::GlobalArgs;
use creddy_cli::{GlobalArgs, RunArgs};
use crate::{
config::{self, AppConfig},
@ -32,7 +32,7 @@ use crate::{
pub static APP: OnceCell<AppHandle> = OnceCell::new();
pub fn run(global_args: GlobalArgs) -> tauri::Result<()> {
pub fn run(run_args: RunArgs, global_args: GlobalArgs) -> tauri::Result<()> {
if let Ok(_) = creddy_cli::show_window(global_args) {
// app is already running, so terminate
return Ok(());
@ -62,7 +62,7 @@ pub fn run(global_args: GlobalArgs) -> tauri::Result<()> {
ipc::get_devmode,
ipc::exit,
])
.setup(|app| rt::block_on(setup(app)))
.setup(|app| rt::block_on(setup(app, run_args)))
.build(tauri::generate_context!())?
.run(|app, run_event| {
if let RunEvent::WindowEvent { event, .. } = run_event {
@ -88,11 +88,11 @@ pub async fn connect_db() -> Result<SqlitePool, SetupError> {
}
async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
async fn setup(app: &mut App, run_args: RunArgs) -> Result<(), Box<dyn Error>> {
APP.set(app.handle().clone()).unwrap();
tray::setup(app)?;
// get_or_create_db_path doesn't create the actual db file, just the directory
let is_first_launch = !config::get_or_create_db_path()?.exists();
let is_first_launch = !config::get_or_create_db_path()?.try_exists()?;
let pool = connect_db().await?;
let mut setup_errors: Vec<String> = vec![];
@ -111,10 +111,16 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
creddy_server::serve(app.handle().clone())?;
agent::serve(app.handle().clone())?;
config::set_auto_launch(conf.start_on_login)?;
if let Err(_e) = config::set_auto_launch(conf.start_on_login) {
setup_errors.push("Error: Failed to manage autolaunch.".into());
// if this is the first launch, setup system with default auto-launch settings
if is_first_launch {
if let Err(e) = conf.set_auto_launch() {
setup_errors.push(format!("Failed to manage autolaunch: {e}"));
}
}
// otherwise, treat the system as the source of truth and ensure ours matches
else {
conf.match_auto_launch(&pool).await?;
};
// if hotkeys fail to register, disable them so that this error doesn't have to keep showing up
if let Err(_e) = shortcuts::register_hotkeys(&conf.hotkeys) {
@ -127,7 +133,7 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
.map(|names| names.split(':').any(|n| n == "GNOME"))
.unwrap_or(false);
if !conf.start_minimized || is_first_launch {
if !run_args.minimized {
show_main_window(&app.handle())?;
}

View File

@ -1,7 +1,7 @@
use std::path::PathBuf;
use std::time::Duration;
use auto_launch::AutoLaunchBuilder;
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use is_terminal::IsTerminal;
use serde::{Serialize, Deserialize};
use sqlx::SqlitePool;
@ -89,29 +89,49 @@ impl AppConfig {
pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> {
kv::save(pool, "config", self).await
}
}
/// Configure system with auto-launch settings
pub fn set_auto_launch(&self) -> Result<(), SetupError> {
let mgr = self.auto_launch_manager()?;
pub fn set_auto_launch(is_configured: bool) -> Result<(), SetupError> {
let path_buf = std::env::current_exe()
.map_err(|e| auto_launch::Error::Io(e))?;
let path = path_buf
.to_string_lossy();
// if enabled, disabled regardless of desired end state because either:
// a) we are just going to leave it disabled, or
// b) we need to disable-and-reenable in case args are different
if mgr.is_enabled()? {
mgr.disable()?;
}
if self.start_on_login {
mgr.enable()?;
}
let auto = AutoLaunchBuilder::new()
.set_app_name("Creddy")
.set_app_path(&path)
.build()?;
let is_enabled = auto.is_enabled()?;
if is_configured && !is_enabled {
auto.enable()?;
}
else if !is_configured && is_enabled {
auto.disable()?;
Ok(())
}
Ok(())
/// Match own auto-launch settings to system
pub async fn match_auto_launch(&mut self, pool: &SqlitePool) -> Result<(), SetupError> {
let mgr = self.auto_launch_manager()?;
let is_enabled = mgr.is_enabled()?;
if is_enabled != self.start_on_login {
self.start_on_login = is_enabled;
self.save(pool).await?;
}
Ok(())
}
fn auto_launch_manager(&self) -> Result<AutoLaunch, SetupError> {
let path_buf = std::env::current_exe()
.map_err(|e| auto_launch::Error::Io(e))?;
let name = if cfg!(debug_assertions) { "Creddy" } else { "Creddy (dev)" };
let mut builder = AutoLaunchBuilder::new();
builder.set_app_name(name);
builder.set_app_path(&path_buf.to_string_lossy());
if self.start_minimized {
builder.set_args(&["run", "--minimized"]);
}
Ok(builder.build()?)
}
}

View File

@ -8,14 +8,23 @@ use creddy::{
app,
errors::ShowError,
};
use creddy_cli::{Action, Cli};
use creddy_cli::{
Action,
Cli,
RunArgs,
};
fn main() {
let cli = Cli::parse();
let res = match cli.action {
None | Some(Action::Run) => {
app::run(cli.global_args).error_popup("Creddy encountered an error");
None => {
let run_args = RunArgs { minimized: false };
app::run(run_args, cli.global_args).error_popup("Creddy encountered an error");
Ok(())
}
Some(Action::Run(run_args)) => {
app::run(run_args, cli.global_args).error_popup("Creddy encountered an error");
Ok(())
},
Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args),

View File

@ -4,6 +4,7 @@ use tauri::{
AppHandle,
async_runtime as rt,
Manager,
Runtime,
};
use tokio::io::AsyncReadExt;
use tokio::sync::oneshot;
@ -80,9 +81,11 @@ impl<'s> CloseWaiter<'s> {
}
fn serve<H, F>(sock_name: &str, app_handle: AppHandle, handler: H) -> std::io::Result<()>
where H: Copy + Send + Fn(Stream, AppHandle, u32) -> F + 'static,
// note: AppHandle is generic over `Runtime` for testing
fn serve<H, F, R>(sock_name: &str, app_handle: AppHandle<R>, handler: H) -> std::io::Result<()>
where H: Copy + Send + Fn(Stream, AppHandle<R>, u32) -> F + 'static,
F: Send + Future<Output = Result<(), HandlerError>>,
R: Runtime
{
let (mut listener, addr) = platform::bind(sock_name)?;
rt::spawn(async move {
@ -223,3 +226,31 @@ mod platform {
Ok((stream, pid))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::AsyncWriteExt;
#[tokio::test]
async fn test_server_connect() {
let app = tauri::test::mock_app();
serve("creddy_server_test", app.app_handle().clone(), |mut stream, _handle, _pid| {
async move {
let buf = serde_json::to_vec(&CliResponse::Empty).unwrap();
stream.write_all(&buf).await.unwrap();
Ok(())
}
}).unwrap();
let addr = creddy_cli::server_addr("creddy_server_test");
let mut stream = creddy_cli::connect(Some(addr)).await.unwrap();
let mut buf = Vec::new();
stream.read_to_end(&mut buf).await.unwrap();
let resp: CliResponse = serde_json::from_slice(&buf).unwrap();
assert!(matches!(resp, CliResponse::Empty))
}
}

View File

@ -22,7 +22,7 @@ use crate::credentials::{
DockerCredential,
SshKey,
};
use crate::{config, config::AppConfig};
use crate::config::AppConfig;
use crate::credentials::{
AwsBaseCredential,
Credential,
@ -204,8 +204,9 @@ impl AppState {
let mut live_config = self.config.write().await;
// update autostart if necessary
if new_config.start_on_login != live_config.start_on_login {
config::set_auto_launch(new_config.start_on_login)?;
if new_config.start_on_login != live_config.start_on_login
|| new_config.start_minimized != live_config.start_minimized {
new_config.set_auto_launch()?;
}
// re-register hotkeys if necessary

View File

@ -50,7 +50,7 @@
}
},
"productName": "creddy",
"version": "0.6.1",
"version": "0.6.3",
"identifier": "creddy",
"plugins": {},
"app": {

View File

@ -20,7 +20,6 @@
let error = null;
async function save() {
try {
throw('wtf');
await invoke('save_config', {config});
$appState.config = await invoke('get_config');
}
@ -41,18 +40,20 @@
<form on:submit|preventDefault={save}>
<div class="max-w-lg mx-auto my-1.5 p-4 space-y-16">
<SettingsGroup name="General">
<SettingsGroup name="General">
<ToggleSetting title="Start on login" bind:value={config.start_on_login}>
<svelte:fragment slot="description">
Start Creddy when you log in to your computer.
</svelte:fragment>
</ToggleSetting>
<ToggleSetting title="Start minimized" bind:value={config.start_minimized}>
<svelte:fragment slot="description">
Minimize to the system tray at startup.
</svelte:fragment>
</ToggleSetting>
{#if config.start_on_login}
<ToggleSetting title="Start minimized" bind:value={config.start_minimized}>
<svelte:fragment slot="description">
Minimize to the system tray when starting on login.
</svelte:fragment>
</ToggleSetting>
{/if}
<NumericSetting title="Re-hide delay" bind:value={config.rehide_ms} min={0} unit="Milliseconds">
<svelte:fragment slot="description">