2 Commits

Author SHA1 Message Date
8f2b9adb3a Merge pull request 'hotfix(critical): single-instance lock — SQLite race 방지 (v0.2.5)' (#23) from hotfix/single-instance-lock into main
Reviewed-on: #23
2026-05-04 15:48:05 +00:00
altair823
7187aea0a9 hotfix(critical): single-instance lock — multi-process SQLite race 방지
dogfood 발견 — 앱 아이콘 클릭 시마다 새 process 가 떠서 트레이 아이콘 여러 개,
SQLite 동시 접근 + AiWorker 중복 처리 + HealthChecker 중복 polling 등
**데이터 corruption 위험**.

원인: app.requestSingleInstanceLock() 호출 부재. Electron default 가
multi-instance 라 .exe 실행마다 별도 process.

Fix:
- app.requestSingleInstanceLock() 첫 줄에서 호출
- 두 번째 인스턴스 → app.quit() 즉시 종료
- 'second-instance' 이벤트 → 기존 inbox 창 restore + show + focus
  (사용자 의도는 "앱 보기" 라 가정)

게이트: typecheck 0 / 단위 413 / e2e 1
version: 0.2.4 → 0.2.5 (critical hotfix patch)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 00:42:50 +09:00
3 changed files with 24 additions and 3 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "inkling",
"version": "0.2.4",
"version": "0.2.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "inkling",
"version": "0.2.4",
"version": "0.2.5",
"dependencies": {
"better-sqlite3": "12.9.0",
"electron-log": "5.2.0",

View File

@@ -1,6 +1,6 @@
{
"name": "inkling",
"version": "0.2.4",
"version": "0.2.5",
"private": true,
"description": "Inkling — local-first 한 줄 보관 도구",
"author": "altair823 <dlsrks0734@gmail.com>",

View File

@@ -36,6 +36,27 @@ import { DEFAULT_OLLAMA_ENDPOINT, DEFAULT_OLLAMA_MODEL } from '../shared/constan
const HIDDEN_ARG = '--hidden';
const startedHidden = process.argv.includes(HIDDEN_ARG);
// CRITICAL — single-instance lock. 두 번째 .exe 실행 시 즉시 종료.
// 미적용 시 SQLite 동시 접근 + AiWorker 중복 처리 + HealthChecker 중복 polling 등
// 데이터 corruption 위험. activate (Mac) / second-instance (Win/Linux) 이벤트로
// 기존 inbox 창에 focus.
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
app.quit();
} else {
app.on('second-instance', () => {
// 새 .exe 실행 또는 트레이 외 entry 시 기존 inbox 창 보이게
const win = getInboxWindow();
if (win) {
if (win.isMinimized()) win.restore();
if (!win.isVisible()) win.show();
win.focus();
} else {
createInboxWindow();
}
});
}
app.whenReady().then(async () => {
initLogger();
logger.info('app.start', {