mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-05 21:50:46 +08:00
Align with the existing @logger alias convention by introducing @application as a short alias for src/main/core/application. This reduces import verbosity across ~130 main-process files while keeping 4 intentional sub-path imports (@main/core/application/Application) unchanged to preserve the vi.mock bypass mechanism in tests. Configured in tsconfig.node.json and electron.vite.config.ts; Vitest inherits the alias automatically. Signed-off-by: fullex <0xfullex@gmail.com>
257 lines
9.5 KiB
Markdown
257 lines
9.5 KiB
Markdown
# Application Overview
|
|
|
|
Application is the top-level orchestrator that ties together the lifecycle system and the Electron app. It is the single entry point for bootstrapping, shutting down, and controlling services at runtime.
|
|
|
|
## Relationship to Lifecycle
|
|
|
|
```
|
|
Application — "what to do" (register services, bootstrap, shutdown, runtime control)
|
|
└── lifecycle/ — "how to do it" (IoC container, dependency resolution, state machine)
|
|
```
|
|
|
|
Application does not duplicate lifecycle logic. It delegates to `ServiceContainer` and `LifecycleManager` internally, while providing a clean, app-level API.
|
|
|
|
For lifecycle internals (phases, hooks, states, decorators, events), see [Lifecycle Overview](./lifecycle-overview.md).
|
|
|
|
## Quick Start
|
|
|
|
Both `application` and `serviceList` are barrel-exported from the `@application` path alias (configured in `tsconfig.node.json` and `electron.vite.config.ts`):
|
|
|
|
```typescript
|
|
import { application, serviceList } from '@application'
|
|
|
|
// 1. Register all services
|
|
application.registerAll(serviceList)
|
|
|
|
// 2. Bootstrap (handles all three phases + Electron lifecycle)
|
|
await application.bootstrap()
|
|
|
|
// 3. Access a service
|
|
const dbService = application.get('DbService')
|
|
```
|
|
|
|
## Bootstrap Flow
|
|
|
|
`application.bootstrap()` orchestrates the full startup sequence:
|
|
|
|
```
|
|
setupSignalHandlers() ← SIGINT/SIGTERM → graceful shutdown
|
|
setupQuitHandlers() ← before-quit (preventQuit gate) + will-quit (shutdown)
|
|
│
|
|
├── startPhase(Background) ← fire-and-forget (non-blocking)
|
|
│
|
|
├── startPhase(BeforeReady) ─┐
|
|
│ ├──── run in parallel
|
|
└── app.whenReady() ─┘
|
|
│
|
|
├── setupElectronHandlers() ← window-all-closed, preventQuit IPC
|
|
│
|
|
├── startPhase(WhenReady) ← services requiring Electron API
|
|
│
|
|
├── await Background ← ensure background services finished
|
|
│
|
|
└── allReady() ← notify all services the system is fully ready
|
|
```
|
|
|
|
If a `fail-fast` service throws during bootstrap, a dialog is shown offering Exit or Restart.
|
|
|
|
## Shutdown Flow
|
|
|
|
`application.shutdown()` is called automatically on:
|
|
- `will-quit` (Electron event, after all windows closed)
|
|
- `SIGINT` / `SIGTERM` (with 5-second force-exit timeout, bypasses Electron event chain)
|
|
|
|
```
|
|
shutdown()
|
|
├── bootConfigService.flush() ← save pending debounced writes
|
|
├── stopAll() ← onStop() in reverse initialization order
|
|
├── destroyAll() ← onDestroy() in reverse initialization order
|
|
└── loggerService.finish() ← close logger (must be last)
|
|
```
|
|
|
|
On non-macOS, `window-all-closed` triggers `application.quit()` which flows through `before-quit` → `will-quit` → `shutdown()`.
|
|
|
|
## Service Registry
|
|
|
|
Services are registered in `serviceRegistry.ts`. Adding a service is one line:
|
|
|
|
```typescript
|
|
// serviceRegistry.ts
|
|
import { NewService } from '@main/services/NewService'
|
|
|
|
export const services = {
|
|
// ... existing services
|
|
NewService, // ← add one line, types are auto-derived
|
|
} as const
|
|
```
|
|
|
|
This gives you type-safe access via `application.get('NewService')`.
|
|
|
|
## Service Access Rules
|
|
|
|
Services managed by the lifecycle system must **not** export singleton instances. The service CLASS is exported for type references only (e.g., `ServiceRegistry`, `@DependsOn`). All runtime access goes through `application.get()` (unconditional services) or `application.getOptional()` (conditional services with `@Conditional`).
|
|
|
|
### Assign to a local variable before use
|
|
|
|
Do **not** chain `application.get('...')` with method calls directly. Assign the service to a local variable first, then use it:
|
|
|
|
```typescript
|
|
// ✗ BAD: chained calls
|
|
application.get('PreferenceService').get('app.zoom_factor')
|
|
application.get('PreferenceService').set('app.zoom_factor', 1)
|
|
|
|
// ✓ GOOD: assign first, then use
|
|
const preferenceService = application.get('PreferenceService')
|
|
preferenceService.get('app.zoom_factor')
|
|
preferenceService.set('app.zoom_factor', 1)
|
|
```
|
|
|
|
This improves readability, avoids repeated container lookups, and makes the code easier to refactor.
|
|
|
|
### Conditional service access
|
|
|
|
Services with `@Conditional` must be accessed via `getOptional()`, which returns `T | undefined`. Using `get()` on a conditional service throws an error, even when the service is active on the current platform — this prevents cross-platform bugs.
|
|
|
|
```typescript
|
|
// ✗ BAD: get() on conditional service — throws even if service is active
|
|
const menu = application.get('AppMenuService')
|
|
|
|
// ✓ GOOD: getOptional() for conditional services
|
|
const menu = application.getOptional('AppMenuService')
|
|
menu?.buildMenu()
|
|
```
|
|
|
|
## Runtime Service Control
|
|
|
|
Control individual services at runtime without restarting the app:
|
|
|
|
```typescript
|
|
// Stop a service (cascades to dependents)
|
|
await application.stop('HeavyComputeService')
|
|
|
|
// Start a stopped service (re-runs onInit, cascades to dependents)
|
|
await application.start('HeavyComputeService')
|
|
|
|
// Restart = stop + start
|
|
await application.restart('HeavyComputeService')
|
|
|
|
// Pause/Resume (service must implement Pausable interface)
|
|
await application.pause('RealTimeService')
|
|
await application.resume('RealTimeService')
|
|
```
|
|
|
|
All operations cascade through the dependency graph automatically.
|
|
|
|
### Cascade Operations
|
|
|
|
When pausing/stopping a service, all services that depend on it are automatically paused/stopped first. When resuming/starting, dependent services are restored in reverse order.
|
|
|
|
```typescript
|
|
// If PreferenceService depends on DbService:
|
|
await application.stop('DbService')
|
|
// → PreferenceService is stopped first, then DbService
|
|
|
|
await application.start('DbService')
|
|
// → DbService is started first, then PreferenceService
|
|
```
|
|
|
|
**Important**: For pause/resume, ALL services in the cascade chain must implement `Pausable`. If any dependent service doesn't, the operation is aborted with an error log.
|
|
|
|
## App Relaunch
|
|
|
|
Always use `application.relaunch()` instead of calling `app.relaunch()` directly. It handles:
|
|
|
|
- **Dev mode detection**: Shows a dialog and exits gracefully (auto-relaunch is not possible in dev)
|
|
- **Platform fixes**: Linux AppImage `execPath` rewrite, Windows Portable executable path
|
|
|
|
```typescript
|
|
import { application } from '@application'
|
|
|
|
// Simple relaunch
|
|
application.relaunch()
|
|
|
|
// With custom options (forwarded to Electron's app.relaunch)
|
|
application.relaunch({ args: ['--safe-mode'] })
|
|
```
|
|
|
|
## App Quit
|
|
|
|
Always use `application.quit()` or `application.forceExit()` instead of calling `app.quit()` / `app.exit()` directly. An ESLint rule (`no-restricted-properties`) will warn if `app.quit()` or `app.exit()` is used in `src/main/` outside of `Application.ts`.
|
|
|
|
```typescript
|
|
import { application } from '@application'
|
|
|
|
// Graceful quit — triggers the Electron before-quit / will-quit event chain
|
|
application.quit()
|
|
|
|
// Force exit — skips the event chain, for fatal/unrecoverable errors only
|
|
application.forceExit(1)
|
|
|
|
// Mark as quitting without triggering quit — for external quit flows (e.g. autoUpdater)
|
|
application.markQuitting()
|
|
|
|
// Prevent quit during critical operations (e.g. data migration)
|
|
const hold = application.preventQuit('Migrating data')
|
|
try { /* critical work */ } finally { hold.dispose() }
|
|
|
|
// Check quit status
|
|
if (application.isQuitting) { /* ... */ }
|
|
```
|
|
|
|
| Method | Event chain | Use case |
|
|
|--------|-------------|----------|
|
|
| `quit()` | Triggers `before-quit` → `will-quit` | Normal user-initiated quit |
|
|
| `forceExit(code)` | Skipped | Fatal errors, repeated renderer crash |
|
|
| `markQuitting()` | None (flag only) | `autoUpdater.quitAndInstall()` owns its own quit flow |
|
|
| `preventQuit(reason)` | Blocks `before-quit` | Critical operations (returns hold with `dispose()`) |
|
|
|
|
**Exceptions** (where direct `app.quit()` is acceptable):
|
|
- Before `application` is initialized (e.g., single-instance lock failure in `index.ts`)
|
|
- Migration files (`src/main/data/migration/`) that run before the full app lifecycle
|
|
|
|
### Renderer Usage
|
|
|
|
The renderer accesses application lifecycle methods via `window.api.application`:
|
|
|
|
```typescript
|
|
// Quit the app (triggers before-quit → will-quit event chain)
|
|
await window.api.application.quit()
|
|
|
|
// Relaunch the app
|
|
await window.api.application.relaunch()
|
|
await window.api.application.relaunch({ args: ['--safe-mode'] })
|
|
|
|
// Prevent quit during critical operations (returns opaque holdId)
|
|
const holdId = await window.api.application.preventQuit('Migrating user data')
|
|
try {
|
|
await performCriticalWork()
|
|
} finally {
|
|
await window.api.application.allowQuit(holdId)
|
|
}
|
|
```
|
|
|
|
| Method | Returns | Description |
|
|
|--------|---------|-------------|
|
|
| `quit()` | `Promise<void>` | Graceful quit via Electron event chain |
|
|
| `relaunch(options?)` | `Promise<void>` | Relaunch the app (with optional args) |
|
|
| `preventQuit(reason)` | `Promise<string>` (holdId) | Block app quit until released |
|
|
| `allowQuit(holdId)` | `Promise<void>` | Release a specific quit prevention hold |
|
|
|
|
## The `application` Proxy
|
|
|
|
The exported `application` constant is a lazy proxy — safe to import at module top level before `bootstrap()` is called. The actual `Application` instance is created on first property access.
|
|
|
|
```typescript
|
|
// Safe to import anywhere, even at module scope
|
|
import { application } from '@application'
|
|
```
|
|
|
|
## File Structure
|
|
|
|
```
|
|
application/
|
|
├── Application.ts # Application singleton + lazy proxy
|
|
├── serviceRegistry.ts # Central service registry (add services here)
|
|
└── index.ts # Barrel export
|
|
```
|