본문으로 건너뛰기

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>) {
// 서버 하트비트 전송
}
}

스케줄러 주기:

루프간격역할
Monitor1초메트릭/창 수집, 캡처 결정, 앱 전환 감지
Metrics5초시스템 메트릭 저장
Process10초프로세스 스냅샷 저장
Sync10초배치 업로드
Heartbeat30초서버 연결 확인
Aggregation1시간메트릭 집계
Notification1분유휴/장시간 작업/고사용량 알림
Focus1분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
WindowsHKCU\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 알림으로 전달
}
}

제안 타입:

타입조건메시지
TakeBreak90분 연속 작업휴식 권장
NeedFocusTime소통 40%+집중 시간 필요
RestoreContext인터럽션 5분+ 경과이전 작업 복귀 안내
ExcessiveCommunication평균 대비 150%+소통 과다 알림

앱 카테고리 분류:

카테고리앱 예시
CommunicationSlack, Teams, Discord, Zoom
DevelopmentVSCode, IntelliJ, Xcode, Terminal
DocumentationNotion, Obsidian, Word, Pages
BrowserChrome, Safari, Firefox
DesignFigma, Sketch, Photoshop
MediaSpotify, YouTube, Netflix
SystemFinder, 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설정 파일 경로

관련 문서: