Skip to content

Commit abd42d7

Browse files
committed
v0.3.5: 런처 안정성 전면 개선 — 서버 크래시 진단 + 프로세스 관리 + 에러 복구
- runner: DARTLAB_LLM_BASE_URL 환경변수 주입 (exit code: 1 근본 원인 수정) - runner: 서버 stdout/stderr를 로그 파일로 캡처 (Stdio::null 제거) - runner: 크래시/타임아웃 시 서버 로그 마지막 15줄 + 경로 에러에 포함 - runner: 포트 충돌 감지 (dartlab vs 다른 프로세스 구분) - ollama: wmic → PowerShell Get-CimInstance 대체 (Windows 11 호환) - ollama: 프로세스 추적 + stop_ollama() (런처가 시작한 것만 종료) - ollama: TCP → HTTP 헬스체크 (/api/tags), 모델 등록 API 검증 - ollama: PATH fallback (설치 후 PATH 미반영 대응) - setup: uv/dartlab 바이너리 실행 검증 (깨진 설치 자동 재설치) - setup: subprocess stderr 로깅, 다운로드 3회 자동 재시도 - state: 웜 스타트 전 바이너리 헬스체크 (실패 시 콜드 전환) - UI: 에러에 로그 경로 자동 포함, 스크롤/선택 가능 - UI: "로그 열기" + "초기화 후 재시도" 버튼 추가 - 콜드 스타트 타임아웃 30s → 60s
1 parent 0ef6900 commit abd42d7

6 files changed

Lines changed: 374 additions & 74 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dartlab-desktop"
3-
version = "0.3.4"
3+
version = "0.3.5"
44
edition = "2024"
55
description = "DartLab AI Desktop Launcher for Windows"
66
license = "MIT"

src/main.rs

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod selfUpdate;
1010
mod state;
1111
mod logger;
1212

13+
use std::process::Command;
1314
use std::sync::mpsc;
1415
use tao::event::{Event, WindowEvent};
1516
use tao::event_loop::{ControlFlow, EventLoopBuilder};
@@ -89,24 +90,41 @@ const SETUP_HTML: &str = r#"<!DOCTYPE html>
8990
color: #ea4647;
9091
font-size: 12px;
9192
margin-top: 8px;
92-
max-width: 400px;
93-
text-align: center;
93+
max-width: 480px;
94+
text-align: left;
9495
display: none;
96+
white-space: pre-wrap;
97+
user-select: all;
98+
overflow-y: auto;
99+
max-height: 180px;
100+
line-height: 1.5;
101+
padding: 0 12px;
102+
word-break: break-all;
95103
}
96-
#retry-btn {
104+
.action-btns {
97105
display: none;
98106
margin-top: 12px;
107+
gap: 8px;
108+
justify-content: center;
109+
flex-wrap: wrap;
110+
}
111+
.action-btns button {
99112
border: 1px solid #ea4647;
100113
background: transparent;
101114
color: #ea4647;
102115
border-radius: 6px;
103-
padding: 8px 20px;
104-
font-size: 13px;
116+
padding: 8px 16px;
117+
font-size: 12px;
105118
cursor: pointer;
106119
font-weight: 500;
107120
transition: background 0.2s;
108121
}
109-
#retry-btn:hover { background: rgba(234,70,71,0.1); }
122+
.action-btns button:hover { background: rgba(234,70,71,0.1); }
123+
.action-btns .btn-secondary {
124+
border-color: #334155;
125+
color: #94a3b8;
126+
}
127+
.action-btns .btn-secondary:hover { background: rgba(148,163,184,0.1); }
110128
#update-banner {
111129
display: none;
112130
position: fixed;
@@ -148,7 +166,11 @@ const SETUP_HTML: &str = r#"<!DOCTYPE html>
148166
<div class="bar-wrap"><div class="bar" id="bar"></div></div>
149167
<div id="status"></div>
150168
<div id="error"></div>
151-
<button id="retry-btn" onclick="window.ipc.postMessage('retry')">다시 시도</button>
169+
<div class="action-btns" id="action-btns">
170+
<button onclick="window.ipc.postMessage('retry')">다시 시도</button>
171+
<button class="btn-secondary" onclick="window.ipc.postMessage('open-log')">로그 열기</button>
172+
<button class="btn-secondary" onclick="window.ipc.postMessage('reset')">초기화 후 재시도</button>
173+
</div>
152174
<div id="update-banner">
153175
<div>
154176
<span class="label" id="update-label"></span><br>
@@ -166,11 +188,11 @@ const SETUP_HTML: &str = r#"<!DOCTYPE html>
166188
var el = document.getElementById('error');
167189
el.textContent = msg;
168190
el.style.display = 'block';
169-
document.getElementById('retry-btn').style.display = 'inline-block';
191+
document.getElementById('action-btns').style.display = 'flex';
170192
}
171193
function clearError() {
172194
document.getElementById('error').style.display = 'none';
173-
document.getElementById('retry-btn').style.display = 'none';
195+
document.getElementById('action-btns').style.display = 'none';
174196
}
175197
function showUpdate(type, ver) {
176198
var label = type === 'launcher' ? '런처' : 'DartLab';
@@ -247,6 +269,32 @@ fn main() {
247269
run_setup(tx, p, us);
248270
});
249271
}
272+
"open-log" => {
273+
if let Some(lp) = logger::log_path() {
274+
let dir = lp.parent().unwrap_or(&lp).to_path_buf();
275+
std::thread::spawn(move || {
276+
let _ = Command::new("explorer")
277+
.arg(dir)
278+
.spawn();
279+
});
280+
}
281+
}
282+
"reset" => {
283+
let _ = ipc_proxy.send_event(AppEvent::Log("clearError()".to_string()));
284+
let p = ipc_proxy.clone();
285+
let us = update_state_ipc.clone();
286+
std::thread::spawn(move || {
287+
let ad = paths::app_dir();
288+
state::clear_state(&ad);
289+
let venv = paths::venv_dir(&ad);
290+
if venv.exists() {
291+
std::fs::remove_dir_all(&venv).ok();
292+
}
293+
logger::log("사용자 초기화 — venv + state 삭제 후 콜드 스타트");
294+
let (tx, _rx) = mpsc::channel::<AppEvent>();
295+
run_setup(tx, p, us);
296+
});
297+
}
250298
"update:dartlab:yes" => {
251299
let state = update_state_ipc.lock().unwrap();
252300
if let Some(ref ad) = state.app_dir {
@@ -335,6 +383,7 @@ fn main() {
335383
..
336384
} => {
337385
runner::stop_server();
386+
ollama::stop_ollama();
338387
*control_flow = ControlFlow::Exit;
339388
}
340389
_ => {}
@@ -365,7 +414,13 @@ fn run_setup(
365414
};
366415

367416
let fail = |msg: &str| {
368-
let escaped = msg.replace('\'', "\\'");
417+
let mut full = msg.to_string();
418+
if let Some(lp) = logger::log_path() {
419+
if !full.contains("로그 파일:") {
420+
full.push_str(&format!("\n\n런처 로그: {}", lp.display()));
421+
}
422+
}
423+
let escaped = full.replace('\\', "\\\\").replace('\'', "\\'").replace('\n', "\\n");
369424
js(&format!("setError('{escaped}')"));
370425
logger::log(&format!("ERROR: {msg}"));
371426
};
@@ -398,7 +453,13 @@ fn run_setup(
398453
});
399454
}
400455

401-
let warm = state::is_warm(&app_dir);
456+
let mut warm = state::is_warm(&app_dir);
457+
458+
if warm && !state::quick_health_check(&app_dir) {
459+
logger::log("웜 스타트 헬스체크 실패 — 콜드 스타트로 전환");
460+
state::clear_state(&app_dir);
461+
warm = false;
462+
}
402463

403464
if warm {
404465
let gpu = ollama::gpu_label();
@@ -433,9 +494,9 @@ fn run_setup(
433494
}
434495

435496
progress(50, "서버 시작 중...");
436-
if let Err(e) = runner::start_server(&app_dir) {
437-
fail(&format!("서버 시작 실패: {e}"));
438-
return;
497+
match runner::start_server(&app_dir) {
498+
Ok(_) => {}
499+
Err(e) => { fail(&format!("서버 시작 실패: {e}")); return; }
439500
}
440501

441502
progress(70, "서버 응답 대기 중...");
@@ -493,13 +554,13 @@ fn run_setup(
493554
}
494555

495556
progress(85, "서버 시작 중...");
496-
if let Err(e) = runner::start_server(&app_dir) {
497-
fail(&format!("서버 시작 실패: {e}"));
498-
return;
557+
match runner::start_server(&app_dir) {
558+
Ok(_) => {}
559+
Err(e) => { fail(&format!("서버 시작 실패: {e}")); return; }
499560
}
500561

501562
progress(90, "서버 응답 대기 중...");
502-
match runner::wait_for_server(30) {
563+
match runner::wait_for_server(60) {
503564
Ok(()) => {
504565
progress(100, "준비 완료!");
505566
state::mark_success(&app_dir);

0 commit comments

Comments
 (0)