build(desktop): цели make, README и устойчивость обёртки к правам на логи
- Makefile: `desktop-check`, `desktop-build` (с ключом подписи обновлений, без ключа — сборка без артефактов обновления) и `desktop-run`; кэши cargo и npm держатся в .cache/, как у остальных инструментов проекта. - desktop/README.md: сборка, запуск, аргументы командной строки, настройки, deep links и открытые пункты Фазы 6. - bundle.targets = all: бандлы по умолчанию для текущей ОС (macOS: .app/.dmg). - Логирование подключается в setup и больше не роняет приложение, если каталог логов недоступен на запись (песочница, домашний каталог только для чтения). - Разбор аргументов вынесен в Cli::from_args и покрыт тестами (--instance, --minimized, --settings, --autostart on|off). - cargo fmt по модулям обёртки.
This commit is contained in:
@@ -18,9 +18,10 @@ mod tray;
|
||||
mod updates;
|
||||
mod window;
|
||||
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use tauri::{Manager, WindowEvent};
|
||||
use tauri::{AppHandle, Manager, WindowEvent};
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
|
||||
|
||||
@@ -47,8 +48,13 @@ struct Cli {
|
||||
|
||||
impl Cli {
|
||||
fn parse() -> Self {
|
||||
Self::from_args(std::env::args().skip(1))
|
||||
}
|
||||
|
||||
/// Разбор аргументов отделён от окружения, чтобы его можно было тестировать.
|
||||
fn from_args<I: IntoIterator<Item = String>>(args: I) -> Self {
|
||||
let mut cli = Self::default();
|
||||
let mut args = std::env::args().skip(1);
|
||||
let mut args = args.into_iter();
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--instance" => cli.instance = args.next(),
|
||||
@@ -91,11 +97,6 @@ pub fn run() {
|
||||
bridge::send(app, &bridge::WebMessage::DeepLink { url });
|
||||
}
|
||||
}))
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
@@ -148,8 +149,45 @@ pub fn run() {
|
||||
.expect("не удалось запустить glchat desktop");
|
||||
}
|
||||
|
||||
/// Подключает логирование: файл в каталоге логов приложения и stdout.
|
||||
///
|
||||
/// Если каталог логов недоступен на запись (например, приложение запущено из
|
||||
/// песочницы или домашний каталог только для чтения), логи остаются только в
|
||||
/// stdout: падать из-за прав на каталог приложение не должно.
|
||||
fn install_logging(app: &AppHandle) {
|
||||
let mut builder = tauri_plugin_log::Builder::default().level(log::LevelFilter::Info);
|
||||
match app.path().app_log_dir() {
|
||||
Ok(dir) if directory_is_writable(&dir) => {
|
||||
builder = builder.target(tauri_plugin_log::Target::new(
|
||||
tauri_plugin_log::TargetKind::LogDir { file_name: None },
|
||||
));
|
||||
}
|
||||
Ok(dir) => eprintln!("каталог логов {} недоступен на запись", dir.display()),
|
||||
Err(error) => eprintln!("не удалось определить каталог логов: {error}"),
|
||||
}
|
||||
if let Err(error) = app.plugin(builder.build()) {
|
||||
eprintln!("не удалось подключить логирование: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Проверяет, можно ли писать в каталог (создаёт и удаляет пробный файл).
|
||||
fn directory_is_writable(dir: &Path) -> bool {
|
||||
if std::fs::create_dir_all(dir).is_err() {
|
||||
return false;
|
||||
}
|
||||
let probe = dir.join(".glchat-write-probe");
|
||||
match std::fs::write(&probe, b"") {
|
||||
Ok(()) => {
|
||||
let _ = std::fs::remove_file(&probe);
|
||||
true
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Начальная настройка: настройки, трей, окно, шорткаты, deep links.
|
||||
fn setup(app: &tauri::AppHandle, cli: &Cli) {
|
||||
install_logging(app);
|
||||
let mut current = settings::load(app);
|
||||
if let Some(instance) = &cli.instance {
|
||||
match instance::normalize(instance) {
|
||||
@@ -187,7 +225,11 @@ fn setup(app: &tauri::AppHandle, cli: &Cli) {
|
||||
log::info!(
|
||||
"обёртка запущена: адрес={}, окно={}, автозапуск={}",
|
||||
current.instance_url,
|
||||
if cli.minimized { "свёрнуто" } else { "видимо" },
|
||||
if cli.minimized {
|
||||
"свёрнуто"
|
||||
} else {
|
||||
"видимо"
|
||||
},
|
||||
current.autostart
|
||||
);
|
||||
|
||||
@@ -241,3 +283,50 @@ fn spawn_autostart_prompt(app: tauri::AppHandle) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse(args: &[&str]) -> Cli {
|
||||
Cli::from_args(args.iter().map(|value| value.to_string()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn разбирает_аргументы_запуска() {
|
||||
let cli = parse(&[
|
||||
"--instance",
|
||||
"https://example.com",
|
||||
"--minimized",
|
||||
"--settings",
|
||||
]);
|
||||
assert_eq!(cli.instance.as_deref(), Some("https://example.com"));
|
||||
assert!(cli.minimized);
|
||||
assert!(cli.settings);
|
||||
assert_eq!(cli.autostart, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn разбирает_аргументы_со_значением_через_равно() {
|
||||
let cli = parse(&["--instance=gl.example.org", "--autostart=off"]);
|
||||
assert_eq!(cli.instance.as_deref(), Some("gl.example.org"));
|
||||
assert_eq!(cli.autostart, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn разбирает_переключатель_автозапуска() {
|
||||
assert_eq!(parse(&["--autostart", "on"]).autostart, Some(true));
|
||||
assert_eq!(parse(&["--autostart", "off"]).autostart, Some(false));
|
||||
assert_eq!(parse(&["--autostart", "мусор"]).autostart, None);
|
||||
// Аргумент без значения не должен ничего ломать.
|
||||
assert_eq!(parse(&["--autostart"]).autostart, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn игнорирует_неизвестные_аргументы() {
|
||||
let cli = parse(&["--unknown", "value", "мусор"]);
|
||||
assert_eq!(cli.instance, None);
|
||||
assert!(!cli.minimized);
|
||||
assert!(!cli.settings);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user