oneshim-app
바이너리 진입점 크레이트. DI 와이어링, 스케줄러, 라이프사이클, 자동 업데이트를 담당합니다.
역할
- 진입점:
main()함수, 애플리케이션 시작 - DI 와이어링: 모든 컴포넌트 조립 및 주입
- 스케줄링: 주기적 태스크 실행
- 라이프사이클: 시작/종료 처리
- 자동 업데이트: GitHub Releases 기반 업데이트
디렉토리 구조
oneshim-app/src/
├── main.rs # 진입점, DI 와이어링
├── scheduler.rs # 8-루프 스케줄러
├── focus_analyzer.rs # Edge Intelligence - 집중도 분석
├── notification_manager.rs # 쿨다운 기반 알림 관리
├── lifecycle.rs # 라이프사이클 - 시그널 처리
├── event_bus.rs # 내부 이벤트 라우팅
├── autostart.rs # 자동 시작 설정
└── updater.rs # 자동 업데이트
실행 흐름
주요 컴포넌트
main.rs
애플리케이션 진입점 및 DI 조립:
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 1. 로깅 초기화
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
// 2. 설정 로드
let config = AppConfig::load()?;
// 3. 컴포넌트 생성 (DI 와이어링)
let token_manager = Arc::new(TokenManager::new(
&config.server.base_url,
&std::env::var("ONESHIM_EMAIL")?,
&std::env::var("ONESHIM_PASSWORD")?,
));
let compressor = Arc::new(AdaptiveCompressor::default());
let api_client = Arc::new(HttpApiClient::new(
&config.server.base_url,
token_manager.clone(),
compressor.clone(),
));
let sse_client = Arc::new(SseStreamClient::new(
&config.server.base_url,
token_manager.clone(),
config.server.sse_max_retry_secs,
));
let storage = Arc::new(SqliteStorage::new(&config.storage.db_path)?);
let system_monitor = Arc::new(SysInfoMonitor::new());
let process_monitor = Arc::new(ProcessTracker::new());
let activity_monitor = Arc::new(ActivityTracker::new(300));
let capture_trigger = Arc::new(SmartCaptureTrigger::new(
config.vision.capture_throttle_ms,
));
let frame_processor = Arc::new(EdgeFrameProcessor::new(/* ... */));
let notifier = Arc::new(DesktopNotifierImpl);
let suggestion_queue = Arc::new(PriorityQueue::new(50));
let suggestion_history = Arc::new(SuggestionHistory::new(100));
// 4. 스케줄러 생성
let scheduler = Scheduler::new(/* 모든 컴포넌트 주입 */);
// 5. 라이프사이클 설정
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
let lifecycle = Lifecycle::new(shutdown_tx);
// 6. 태스크 시작
let monitor_task = tokio::spawn(scheduler.run_monitor_loop(shutdown_rx.clone()));
let sync_task = tokio::spawn(scheduler.run_sync_loop(shutdown_rx.clone()));
let sse_task = tokio::spawn(/* SSE 연결 */);
// 7. 종료 시그널 대기
lifecycle.wait_for_shutdown().await;
// 8. 정리
monitor_task.abort();
sync_task.abort();
sse_task.abort();
Ok(())
}
Scheduler (scheduler.rs)
8개의 주기적 태스크 루프:
pub struct Scheduler {
system_monitor: Arc<dyn SystemMonitor>,
process_monitor: Arc<dyn ProcessMonitor>,
activity_monitor: Arc<dyn ActivityMonitor>,
capture_trigger: Arc<dyn CaptureTrigger>,
frame_processor: Arc<dyn FrameProcessor>,
api_client: Arc<dyn ApiClient>,
storage: Arc<dyn StorageService>,
batch_uploader: Arc<BatchUploader>,
config: MonitorConfig,
}
impl Scheduler {
/// 모니터링 루프 (1초 간격)
pub async fn run_monitor_loop(&self, mut shutdown: watch::Receiver<bool>) {
let mut interval = tokio::time::interval(
Duration::from_millis(self.config.poll_interval_ms)
);
loop {
tokio::select! {
_ = interval.tick() => {
if let Err(e) = self.monitor_tick().await {
warn!("모니터링 오류: {}", e);
}
}
_ = shutdown.changed() => {
if *shutdown.borrow() {
info!("모니터링 루프 종료");
break;
}
}
}
}
}
async fn monitor_tick(&self) -> Result<(), CoreError> {
// 1. 시스템 메트릭 수집
let metrics = self.system_monitor.get_metrics().await?;
// 2. 활성 창 확인
let window = self.process_monitor.get_active_window().await?;
// 3. 이벤트 생성
let event = create_context_event(window, metrics, self.activity_monitor.is_idle().await?);
// 4. 로컬 저장
self.storage.save_event(&event).await?;
// 5. 캡처 결정
if let CaptureDecision::Capture { importance } =
self.capture_trigger.should_capture(&event).await?
{
let frame = ScreenCapture::capture_active_window()?;
let processed = self.frame_processor.process(frame).await?;
self.storage.save_frame(&processed).await?;
self.batch_uploader.queue_frame(processed).await;
}
// 6. 배치 큐에 추가
self.batch_uploader.queue_event(event).await;
Ok(())
}
/// 동기화 루프 (10초 간격)
pub async fn run_sync_loop(&self, mut shutdown: watch::Receiver<bool>) {
let mut interval = tokio::time::interval(
Duration::from_millis(self.config.sync_interval_ms)
);
loop {
tokio::select! {
_ = interval.tick() => {
if let Err(e) = self.batch_uploader.flush().await {
warn!("동기화 오류: {}", e);
}
}
_ = shutdown.changed() => break,
}
}
}
/// 하트비트 루프 (30초 간격)
pub async fn run_heartbeat_loop(&self, mut shutdown: watch::Receiver<bool>) {
// 서버 하트비트 전송
}
}
스케줄러 주기:
| 루프 | 간격 | 역할 |
|---|---|---|
| Monitor | 1초 | 메트릭/창 수집, 캡처 결정, 앱 전환 감지 |
| Metrics | 5초 | 시스템 메트릭 저장 |
| Process | 10초 | 프로세스 스냅샷 저장 |
| Sync | 10초 | 배치 업로드 |
| Heartbeat | 30초 | 서버 연결 확인 |
| Aggregation | 1시간 | 메트릭 집계 |
| Notification | 1분 | 유휴/장시간 작업/고사용량 알림 |
| Focus | 1분 | Edge Intelligence 집중도 분석 |
Lifecycle (lifecycle.rs)
시그널 처리 및 graceful shutdown:
pub struct Lifecycle {
shutdown_tx: watch::Sender<bool>,
}
impl Lifecycle {
pub fn new(shutdown_tx: watch::Sender<bool>) -> Self {
Self { shutdown_tx }
}
pub async fn wait_for_shutdown(&self) {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("Ctrl+C 핸들러 설치 실패");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("SIGTERM 핸들러 설치 실패")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => info!("Ctrl+C 수신"),
_ = terminate => info!("SIGTERM 수신"),
}
info!("종료 시작...");
let _ = self.shutdown_tx.send(true);
}
}
EventBus (event_bus.rs)
내부 이벤트 라우팅:
pub struct EventBus {
sender: broadcast::Sender<InternalEvent>,
}
#[derive(Clone, Debug)]
pub enum InternalEvent {
NewSuggestion(Suggestion),
ConnectionStatusChanged(ConnectionStatus),
SyncCompleted { events: usize, frames: usize },
ErrorOccurred(String),
}
impl EventBus {
pub fn new() -> Self {
let (sender, _) = broadcast::channel(100);
Self { sender }
}
pub fn publish(&self, event: InternalEvent) {
let _ = self.sender.send(event);
}
pub fn subscribe(&self) -> broadcast::Receiver<InternalEvent> {
self.sender.subscribe()
}
}
Autostart (autostart.rs)
로그인 시 자동 시작 설정:
pub struct Autostart;
impl Autostart {
#[cfg(target_os = "macos")]
pub fn enable() -> Result<(), CoreError> {
let plist = Self::generate_launchagent_plist()?;
let path = dirs::home_dir()
.unwrap()
.join("Library/LaunchAgents/com.oneshim.client.plist");
std::fs::write(&path, plist)?;
Ok(())
}
#[cfg(target_os = "windows")]
pub fn enable() -> Result<(), CoreError> {
use windows_sys::Win32::System::Registry::*;
let exe_path = std::env::current_exe()?;
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
unsafe {
let mut hkey: HKEY = std::ptr::null_mut();
RegOpenKeyExW(HKEY_CURRENT_USER, /* ... */)?;
RegSetValueExW(hkey, "ONESHIM", /* exe_path */)?;
RegCloseKey(hkey);
}
Ok(())
}
}
플랫폼별 자동 시작:
| 플랫폼 | 방법 |
|---|---|
| macOS | ~/Library/LaunchAgents/com.oneshim.client.plist |
| Windows | HKCU\Software\Microsoft\Windows\CurrentVersion\Run |
| Linux | ~/.config/autostart/oneshim.desktop |
Updater (updater.rs)
GitHub Releases 기반 자동 업데이트:
pub struct Updater {
config: UpdateConfig,
http_client: reqwest::Client,
}
impl Updater {
pub async fn check_for_update(&self) -> Result<Option<Release>, CoreError> {
let url = format!(
"https://api.github.com/repos/{}/{}/releases/latest",
self.config.repo_owner,
self.config.repo_name,
);
let response: GitHubRelease = self.http_client
.get(&url)
.header("User-Agent", "ONESHIM-Client")
.send()
.await?
.json()
.await?;
let current = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
let latest = semver::Version::parse(
&response.tag_name.trim_start_matches('v')
)?;
if latest > current {
Ok(Some(Release {
version: latest,
download_url: Self::find_asset_url(&response)?,
release_notes: response.body,
}))
} else {
Ok(None)
}
}
pub async fn download_and_install(&self, release: &Release) -> Result<(), CoreError> {
// 1. 에셋 다운로드
// 2. 임시 디렉토리에 압축 해제
// 3. 바이너리 교체 (self_update 사용)
Ok(())
}
}
업데이트 흐름:
FocusAnalyzer (focus_analyzer.rs)
Edge Intelligence 기반 집중도 분석 및 로컬 제안 생성:
pub struct FocusAnalyzer {
config: FocusAnalyzerConfig,
storage: Arc<SqliteStorage>,
notifier: Arc<dyn DesktopNotifier>,
tracker: RwLock<SessionTracker>,
cooldowns: RwLock<SuggestionCooldowns>,
}
impl FocusAnalyzer {
/// 앱 전환 시 호출 - 작업 세션/인터럽션 추적
pub async fn on_app_switch(&self, new_app: &str) {
// 1. 앱 카테고리 분류 (Communication, Development, ...)
// 2. 작업 세션 시작/종료 판단
// 3. 인터럽션 기록 (깊은 작업 → 소통 전환 시)
// 4. deep_work_secs 누적
}
/// 1분마다 호출 - 집중도 분석 및 제안 생성
pub async fn analyze_periodic(&self) {
// 1. 오늘 집중도 메트릭 업데이트
// 2. 집중 점수 계산
// 3. 제안 생성 (휴식, 집중 시간, 컨텍스트 복원)
// 4. 쿨다운 기반 중복 방지 (30분)
// 5. OS 알림으로 전달
}
}
제안 타입:
| 타입 | 조건 | 메시지 |
|---|---|---|
TakeBreak | 90분 연속 작업 | 휴식 권장 |
NeedFocusTime | 소통 40%+ | 집중 시간 필요 |
RestoreContext | 인터럽션 5분+ 경과 | 이전 작업 복귀 안내 |
ExcessiveCommunication | 평균 대비 150%+ | 소통 과다 알림 |
앱 카테고리 분류:
| 카테고리 | 앱 예시 |
|---|---|
Communication | Slack, Teams, Discord, Zoom |
Development | VSCode, IntelliJ, Xcode, Terminal |
Documentation | Notion, Obsidian, Word, Pages |
Browser | Chrome, Safari, Firefox |
Design | Figma, Sketch, Photoshop |
Media | Spotify, YouTube, Netflix |
System | Finder, Settings, Activity Monitor |
의존성
| 크레이트 | 용도 |
|---|---|
anyhow | 바이너리 에러 처리 |
tokio | 비동기 런타임 |
tracing-subscriber | 로깅 |
config | 설정 파일 파싱 |
directories | 플랫폼별 디렉토리 |
self_update | 바이너리 업데이트 |
semver | 버전 비교 |
빌드 및 실행
# 개발 빌드
cargo build -p oneshim-app
# 릴리즈 빌드
cargo build --release -p oneshim-app
# 실행
cargo run -p oneshim-app
환경 변수
| 변수 | 필수 | 설명 |
|---|---|---|
ONESHIM_EMAIL | ✅ | 로그인 이메일 |
ONESHIM_PASSWORD | ✅ | 로그인 비밀번호 |
RUST_LOG | ❌ | 로그 레벨 (기본: info) |
ONESHIM_CONFIG | ❌ | 설정 파일 경로 |
관련 문서:
- 클라이언트 개요
- oneshim-core - 설정 및 포트
- oneshim-network - 네트워크 컴포넌트