公開済みのソース
ソースの ZIP をダウンロード
CH-J Tools
特定のアプリケーションリリースのディレクトリとファイルを閲覧できます。
CH-J / Server Manager
1704 ファイル
0.1.2-beta
追加日時: · ビルド日時:
1
//! Application start (replaces Electron `bootstrap()`): data-directory guard and backup,2
//! mandatory license dialog, then Core composition and the main window.4
use crate::commands::license::{LicenseFlow, LicenseSlot};5
use crate::core::app::{BuildInfo, Core, CoreSlot};6
use crate::core::availability::{MonitorHost, Outcome, Target, DEFAULT_INTERVAL_SECS, JUMP_TIMEOUT};7
use crate::core::biometrics::BiometricService;8
use crate::core::config::ConfigStore;9
use crate::core::diagnostics::service::DiagnosticsService;10
use crate::core::i18n::I18n;11
use crate::core::legal::LicenseAcceptance;12
use crate::core::lock::LockController;13
use crate::core::logging::Logger;14
use crate::core::migration;15
use crate::core::modules::installer::ModuleInstaller;16
use crate::core::modules::registry::ModuleRegistry;17
use crate::core::modules::service::ModuleService;18
use crate::core::net::https::{HttpsClient, INSECURE_LEGACY_ORIGINS};19
use crate::core::paths::AppPaths;20
use crate::core::profile_transfer::ProfileTransfer;21
use crate::core::profiles::ProfileService;22
use crate::core::services::Services;23
use crate::core::ssh::privileged::PrivilegedExecutor;24
use crate::core::ssh::session::SessionManager;25
use crate::core::ssh::windows_exec::WindowsExecutor;26
use crate::core::vault::VaultStore;27
use crate::security::caller::WindowRegistry;28
use serde_json::{json, Value};29
use std::sync::Arc;30
use tauri::{AppHandle, Emitter, Manager};31
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};33
/// Held for the whole process lifetime.34
pub struct RuntimeGuard(#[allow(dead_code)] pub migration::RuntimeLock);36
pub struct Prepared {37
pub paths: AppPaths,38
pub logger: Arc<Logger>,39
pub i18n: Arc<I18n>,40
pub config: Arc<ConfigStore>,41
pub build: BuildInfo,42
}44
fn fatal(app: &AppHandle, message: &str) {45
let handle = app.clone();46
app.dialog().message(message).title("CH-J Tools").kind(MessageDialogKind::Error).show(move |_| handle.exit(1));47
}49
pub fn start(app: &AppHandle) {50
let resources = app.path().resource_dir().unwrap_or_default();51
// Renaming the old "CH-J Server Manager" data folder can fail on Windows while its helper still runs.52
let stop_helper = |legacy: &std::path::Path| {53
let _ = crate::core::standby::stop(legacy);54
};55
let paths = match AppPaths::resolve(app.path().config_dir().ok(), resources, stop_helper) {56
Ok(paths) => paths,57
Err(error) => return fatal(app, &error),58
};59
if migration::legacy_electron_running(&paths.data) {60
return fatal(61
app,62
"CH-J Server Manager 0.1.0 is still running and uses the same data. Quit it first, then start the application again.\n\nCH-J Server Manager 0.1.0 stále běží nad stejnými daty. Nejprve ho ukončete a aplikaci spusťte znovu.",63
);64
}65
match migration::acquire_runtime_lock(&paths.data) {66
Ok(lock) => {67
app.manage(RuntimeGuard(lock));68
}69
Err(_) if paths.relocation.as_ref().is_some_and(|r| r.outcome == "kept-legacy-in-use") => {70
return fatal(71
app,72
"CH-J Server Manager (the previous name of this application) is still running and uses your data. Quit it, then start CH-J Tools again. Your data is moved to CH-J Tools at that start.\n\nCH-J Server Manager (předchozí název této aplikace) stále běží a používá vaše data. Ukončete ho a spusťte CH-J Tools znovu; data se při tom přesunou do CH-J Tools.",73
);74
}75
Err(error) => return fatal(app, &error.message),76
}77
let logger = Arc::new(Logger::new(&paths.data));78
if let Some(relocation) = &paths.relocation {79
logger.info("Data folder of the renamed application checked.", serde_json::to_value(relocation).unwrap_or(Value::Null));80
}81
let build = BuildInfo::current(&app.package_info().version.to_string());82
let backup = migration::prepare_data_directory(&paths.data, &build.version, &build.build_id);83
let _ = std::fs::remove_dir_all(paths.legacy_module_webview_data());84
match &backup {85
Ok(report) => logger.info("Data directory prepared.", serde_json::to_value(report).unwrap_or(Value::Null)),86
Err(error) => logger.error("Pre-update backup failed.", json!({ "code": error.code, "message": error.message })),87
}88
let assets = app.asset_resolver();89
let i18n = Arc::new(I18n::load(|path| assets.get(path.to_string()).map(|asset| asset.bytes().to_vec())));90
let config = Arc::new(ConfigStore::new(&paths.data, i18n.codes()));91
config.load();92
let prepared = Prepared { paths, logger, i18n, config, build };93
if let Err(error) = backup {94
// The data was not modified; continuing is the user's explicit decision.95
let handle = app.clone();96
let message = format!(97
"The automatic backup before this update failed ({}). Your data was not changed.\n\nContinue without a new backup?\n\nAutomatická záloha před aktualizací selhala ({}). Data nebyla změněna. Pokračovat bez nové zálohy?",98
error.code, error.code99
);100
app.dialog()101
.message(message)102
.title("CH-J Tools")103
.kind(MessageDialogKind::Warning)104
.buttons(MessageDialogButtons::OkCancelCustom("Continue / Pokračovat".into(), "Quit / Ukončit".into()))105
.show(move |proceed| if proceed { license_then_core(&handle, prepared) } else { handle.exit(0) });106
return;107
}108
license_then_core(app, prepared);109
}111
fn license_then_core(app: &AppHandle, prepared: Prepared) {112
let acceptance = match LicenseAcceptance::new(113
&prepared.paths.data,114
&prepared.paths.licenses().join("APPLICATION_LICENSE.txt"),115
&prepared.paths.docs(),116
&prepared.build.version,117
&prepared.build.build_id,118
) {119
Ok(acceptance) => Arc::new(acceptance),120
Err(error) => return fatal(app, &error.message),121
};122
if acceptance.is_accepted() {123
compose(app, prepared);124
return;125
}126
let (tx, rx) = tokio::sync::oneshot::channel();127
*app.state::<LicenseSlot>().0.lock() = Some(LicenseFlow { acceptance, language: prepared.config.language(), presented: false, shown: false, accepted: false, done: Some(tx) });128
if let Err(error) = crate::windows::create_license_window(app, &prepared.i18n, &prepared.config.language()) {129
return fatal(app, &error.to_string());130
}131
let handle = app.clone();132
tauri::async_runtime::spawn(async move {133
let accepted = rx.await.unwrap_or(false);134
if accepted {135
compose(&handle, prepared);136
if let Some(window) = handle.get_webview_window(crate::security::caller::LICENSE_LABEL) {137
let _ = window.close();138
}139
} else {140
handle.exit(0);141
}142
});143
}145
/// License window closed: resolve the flow (accepted or declined/closed).146
pub fn license_window_closed(app: &AppHandle) {147
let flow = app.state::<LicenseSlot>().0.lock().take();148
if let Some(mut flow) = flow {149
if let Some(done) = flow.done.take() {150
let _ = done.send(flow.accepted);151
}152
}153
}155
fn compose(app: &AppHandle, prepared: Prepared) {156
let Prepared { paths, logger, i18n, config, build } = prepared;157
let vault = Arc::new(VaultStore::new(&paths.data));158
// An update can move the helper (macOS 0.1.2: Contents/MacOS → Contents/Library/LoginItems/CH-J-Standby-Helper.app),159
// so the installed app rewrites the helper's system entries once per start. Development builds never touch them.160
#[cfg(not(debug_assertions))]161
let _ = crate::core::standby::enable_on_first_start(&paths.data);162
#[cfg(not(debug_assertions))]163
if crate::core::standby::load(&paths.data)["enabled"] == true {164
let data = paths.data.clone();165
std::thread::spawn(move || {166
let _ = crate::core::standby::apply(&data);167
});168
}169
let profiles = Arc::new(ProfileService::new(vault.clone()));170
let main_emitter = {171
let handle = app.clone();172
Arc::new(move |event: &str, payload: Value| {173
let _ = handle.emit_to(tauri::EventTarget::webview_window(crate::security::caller::MAIN_LABEL), event, payload);174
})175
};176
let sessions = SessionManager::new(profiles.clone(), logger.clone(), main_emitter.clone());177
let base_urls: Vec<String> = config.get()["updates"]["baseUrls"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();178
let https = match HttpsClient::new(&base_urls, &INSECURE_LEGACY_ORIGINS) {179
Ok(client) => Arc::new(client),180
Err(error) => return fatal(app, &error.message),181
};182
let registry = Arc::new(ModuleRegistry::new(&paths.data, Some(paths.bundled_modules()), logger.clone()));183
let installer = ModuleInstaller::new(&paths.data, &build.version, crate::core::app::MODULE_API_VERSION);184
let biometrics = {185
let config_auth = config.clone();186
let config_reason = config.clone();187
let i18n_reason = i18n.clone();188
Arc::new(BiometricService::new(189
&paths.data,190
&paths.resources,191
vault.clone(),192
Box::new(move || config_auth.get()["security"]["requireSystemAuthentication"] == true),193
Box::new(move || i18n_reason.t(&config_reason.language(), "native.biometric.reason")),194
))195
};196
let diagnostics = {197
let emitter = main_emitter.clone();198
Arc::new(DiagnosticsService::new(&paths.data, paths.resources.clone(), Arc::new(move |event: Value| emitter("diagnostics:progress", event))))199
};200
let updates = {201
let config_for = config.clone();202
let https_for = https.clone();203
let logger_for = logger.clone();204
let root = paths.data.join("updates").join("downloads");205
let verifier = Arc::new(crate::core::updates::openpgp::OpenPgpVerifier::new(paths.resources.join("signing").join("ch-j-signing-public.asc")));206
let (platform, arch) = crate::core::diagnostics::http::node_platform_arch();207
Arc::new(crate::core::updates::service::UpdateService::new(208
&build.version,209
&build.build_id,210
&platform,211
&arch,212
config.clone(),213
Box::new(move || {214
let urls: Vec<String> = config_for.get()["updates"]["baseUrls"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();215
crate::core::updates::provider::ReleaseProvider::new(&urls, https_for.clone(), root.clone(), verifier.clone(), logger_for.clone())216
}),217
logger.clone(),218
))219
};220
let privileged = Arc::new(PrivilegedExecutor::new(sessions.clone()));221
let files = Arc::new(crate::core::files::service::RemoteFileService::new(sessions.clone(), privileged.clone(), logger.clone()));222
let transfers = {223
let handle = app.clone();224
crate::core::files::transfers::TransferManager::new(225
sessions.clone(),226
files.clone(),227
Some(&paths.data),228
logger.clone(),229
// Transfer progress goes only to the module that owns the jobs; jobs keep running while it is closed.230
Arc::new(move |owner: &str, update: Value| {231
if let Some(core) = handle.state::<CoreSlot>().try_get() {232
for context in core.windows.modules().into_iter().filter(|c| c.id == owner) {233
core.emit_window(&context.label, "module:files:transfers:update", update.clone());234
}235
}236
}),237
)238
};239
{240
let transfers = transfers.clone();241
tauri::async_runtime::spawn(async move { transfers.start_session_listener() });242
}243
let windows_executor = Arc::new(WindowsExecutor { sessions: sessions.clone() });244
let users = Arc::new(crate::core::users::service::UserAdmin {245
sessions: sessions.clone(),246
linux: crate::core::users::linux::LinuxUsers { sessions: sessions.clone(), executor: privileged.clone(), logger: logger.clone() },247
windows: crate::core::users::windows::WindowsUsers { sessions: sessions.clone(), executor: windows_executor.clone(), logger: logger.clone() },248
});249
let virtual_memory = Arc::new(crate::core::system::virtual_memory::VirtualMemoryService::new(sessions.clone(), privileged.clone(), logger.clone()));250
let disks = Arc::new(crate::core::disks::service::DiskService::new(sessions.clone(), privileged.clone(), logger.clone()));251
let rsync = crate::core::rsync::service::RsyncService::new(sessions.clone(), privileged.clone(), logger.clone(), &paths.data, std::env::current_exe().unwrap_or_default());252
{253
let rsync = rsync.clone();254
tauri::async_runtime::spawn(async move { rsync.start_scheduler() });255
}256
let network = crate::core::network::service::NetworkService::new(sessions.clone(), privileged.clone(), logger.clone());257
let services = Services {258
network,259
rsync,260
availability: Default::default(),261
keysync: crate::core::keysync::KeySync::new(&paths.data),262
suggest: Arc::new(crate::core::suggest::SuggestService::new(sessions.clone(), vault.clone())),263
disks,264
virtual_memory,265
users,266
files,267
transfers,268
updates,269
https,270
privileged,271
windows: windows_executor,272
sessions,273
modules: Arc::new(ModuleService::new(registry, installer)),274
biometrics,275
diagnostics,276
lock: LockController::default(),277
keys: Default::default(),278
hashing: {279
let handle = app.clone();280
Arc::new(crate::core::hashing::service::LocalHashService::new(281
Arc::new(move |module_id: &str, job: Value| {282
if let Some(core) = handle.state::<CoreSlot>().try_get() {283
for context in core.windows.modules().into_iter().filter(|c| c.id == module_id) {284
core.emit_window(&context.label, "module:hashing:progress", job.clone());285
}286
}287
}),288
Some(logger.clone()),289
))290
},291
};292
let core = Arc::new(Core {293
app: app.clone(),294
paths,295
build,296
logger,297
i18n,298
config,299
vault,300
profiles,301
profile_transfer: ProfileTransfer::default(),302
windows: Arc::new(WindowRegistry::default()),303
services,304
});305
match core.services.updates.cleanup_downloads() {306
Ok(true) => core.logger.info("Core update download cache cleaned on startup.", json!({})),307
Ok(false) => {}308
Err(error) => core.logger.warn("Core update download cache cleanup failed.", json!({ "message": error.message })),309
}310
match core.services.modules.cleanup_downloads(&core) {311
Ok(true) => core.logger.info("plugin package download cache cleaned on startup.", json!({})),312
Ok(false) => {}313
Err(error) => core.logger.warn("plugin package download cache cleanup failed.", json!({ "message": error.message })),314
}315
app.state::<CoreSlot>().set(core.clone());316
let quick = app.state::<crate::core::quick::QuickSlot>();317
let quick_only = quick.get().is_some() && *quick.only_quick.lock();318
let quick_pending = quick.get().is_some();319
if quick_pending && !quick_only {320
// Forwarded during start-up of a normal launch: show both.321
let _ = crate::windows::open_quick_window(app);322
}323
let window_started = std::time::Instant::now();324
let opened = if quick_only { crate::windows::open_quick_window(app) } else { crate::windows::create_main_window(app, &core.i18n, &core.config.language()) };325
core.logger.info("Start-up window created.", json!({ "quick": quick_only, "ms": window_started.elapsed().as_millis() as u64 }));326
match opened {327
Ok(window) => {328
#[cfg(windows)]329
if let Ok(hwnd) = window.hwnd() {330
core.services.biometrics.set_window_handle(Some((hwnd.0 as usize).to_string()));331
}332
let _ = window;333
}334
Err(error) => return fatal(app, &error.to_string()),335
}336
let monitor = core.services.availability.clone();337
let monitor_host = Arc::new(CoreMonitorHost(Arc::downgrade(&core)));338
tauri::async_runtime::spawn(monitor.run(monitor_host));339
// KeySyncManager: queue and verified server state for keys.ch-j.de, every 90 s and right after a new job.340
let keysync = core.services.keysync.clone();341
let sync_core = Arc::downgrade(&core);342
tauri::async_runtime::spawn(async move {343
loop {344
let Some(core) = sync_core.upgrade() else { break };345
if let Ok(transport) = crate::core::keysync::Transport::https() {346
if keysync.run_once(&transport).await {347
keysync.apply_to_vault(&core.vault);348
core.emit_main("keychain:changed", json!({}));349
}350
}351
drop(core);352
tokio::select! { _ = tokio::time::sleep(crate::core::keysync::INTERVAL) => {}, _ = keysync.notify.notified() => {} }353
}354
});355
// Helper availability monitor: keep `standby-servers.json` current while the vault is unlocked (written only on356
// change; off removes it). See `core::standby::sync_servers`.357
let standby_sync = core.clone();358
tauri::async_runtime::spawn(async move {359
loop {360
tokio::time::sleep(std::time::Duration::from_secs(30)).await;361
let profiles = if standby_sync.vault.is_unlocked() { standby_sync.profiles.list().ok() } else { None };362
let data = standby_sync.paths.data.clone();363
let _ = tokio::task::spawn_blocking(move || crate::core::standby::sync_servers(&data, profiles.as_deref())).await;364
}365
});366
// Inactivity auto-lock check every 5 s.367
let checker = core.clone();368
tauri::async_runtime::spawn(async move {369
loop {370
tokio::time::sleep(std::time::Duration::from_secs(5)).await;371
checker.services.lock.check(&checker).await;372
}373
});374
core.logger.info(375
"Core initialized.",376
json!({ "version": core.build.version, "buildId": core.build.build_id, "platform": crate::windows::platform_name(), "arch": std::env::consts::ARCH, "storageRoot": core.paths.data.to_string_lossy(), "runtime": "tauri" }),377
);378
}380
/// The availability monitor's view of the Core (`Weak`: the task must not keep the Core alive).381
struct CoreMonitorHost(std::sync::Weak<Core>);383
impl MonitorHost for CoreMonitorHost {384
fn available(&self) -> bool {385
self.0.upgrade().is_some_and(|core| core.vault.is_unlocked() && core.app.get_window(crate::security::caller::MAIN_LABEL).is_some())386
}387
fn settings(&self) -> (bool, std::time::Duration) {388
let config = self.0.upgrade().map(|core| core.config.get()).unwrap_or_default();389
(config["monitoring"]["enabled"].as_bool() != Some(false), std::time::Duration::from_secs(config["monitoring"]["intervalSeconds"].as_u64().unwrap_or(DEFAULT_INTERVAL_SECS)))390
}391
fn targets(&self) -> Option<Vec<Target>> {392
let profiles = self.0.upgrade()?.profiles.list().ok()?;393
Some(profiles.iter().filter_map(Target::from_profile).collect())394
}395
fn publish(&self, event: Value) {396
if let Some(core) = self.0.upgrade() {397
core.emit_main("availability:changed", event);398
}399
}400
fn probe_jump(&self, jump_id: &str, target: &Target) -> futures::future::BoxFuture<'static, Outcome> {401
let (core, jump_id, host, port) = (self.0.upgrade(), jump_id.to_string(), target.host.clone(), target.port);402
Box::pin(async move {403
match core {404
Some(core) => core.services.sessions.probe_through_jump(&jump_id, &host, port, JUMP_TIMEOUT).await,405
None => Outcome::Unverified { reason: "noJumpSession" },406
}407
})408
}409
}411
/// Orderly shutdown (Electron `before-quit`).412
pub fn shutdown(core: &Arc<Core>) {413
core.services.availability.stop();414
core.services.transfers.shutdown();415
core.services.biometrics.cancel();416
core.services.diagnostics.stop_all();417
let sessions = core.services.sessions.clone();418
tauri::async_runtime::block_on(async move {419
let _ = tokio::time::timeout(std::time::Duration::from_secs(3), sessions.disconnect_all("app-quit")).await;420
});421
core.vault.lock();422
}SHA-256
cafa7226ad2da5e11e564ba0815c964344c3eea336ffb8a7b43b8b0dce9a2646CH-J Proprietary Software License 1.14 · ソースは CH-J Proprietary Software License 1.14 に基づいて提供されています。公開されていても、ライセンス条項が変わったり、追加の権利が付与されたりすることはありません。
アーカイブの SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f