개발 취재 노트
시작 프로그램에 넣은 VBS 파이썬 스크립트가 가끔 에러 창을 띄우고, 부팅 때만 조용히 안 뜰 때
윈도우에서 늘 켜져 있다가 호출어를 들으면 대답하는 로컬 음성 비서(voice-assistant)를 만들고 있었다. 창 없이 뜨게 하려고 시작 프로그램 폴더에 숨김 VBS를 넣어 파이썬(pythonw)을 띄웠는데, 가끔 VBS 에러 창이 뜨고, 어떤 날은 부팅 후 비서가 아예 뜨지 않았다. 로그에는 아무것도 남지 않았다.
결론
On Error Resume Next는 에러 창만 없앤다. 원인은 그대로 남고, 다음에 실패하면 이제 아무 흔적도 없다 .- 부팅 때만 실패하면 부팅 때와 같은 경로(
wscript→ VBS)로 여러 번 돌려 본다. 배치나python.exe를 직접 돌리면 매번 성공해서 문제가 안 보인다 . - 이 사건에서 실패는 비결정적이었고 근본 원인은 끝내 확정하지 못했다 . 그래서 원인을 고치는 대신 실행 방식을 바꿨다.
- 시작 폴더 VBS → 작업 스케줄러: 로그온 45초 뒤 실행, 실패하면 1분 간격 최대 3회 재시도, 이미 실행 중이면 새 실행 무시
- 앱 쪽: 시작할 때마다 타임스탬프 로그, 입력 장치를 최대 60초 재시도, Windows 뮤텍스로 인스턴스 하나만 허용
- 스케줄러가 배치(cmd)를 거치면 창이 남을 수 있다.
pythonw를 직접 실행하고 로그는 파이썬 안에서 파일로 쓴다
아래는 위 설정을 PowerShell로 옮긴 예시다(세션의 원문 코드가 아니다. 경로는 자기 환경에 맞게 바꾼다).
$action = New-ScheduledTaskAction -Execute "C:\myapp\.venv\Scripts\pythonw.exe" `
-Argument "-m src.main" -WorkingDirectory "C:\myapp"
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
$trigger.Delay = "PT45S" # 로그온 45초 뒤
$settings = New-ScheduledTaskSettingsSet `
-RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) `
-MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName "MyAssistant" -Action $action -Trigger $trigger -Settings $settings
# 단일 인스턴스 가드 예시 — 두 번째로 뜬 프로세스는 스스로 종료한다
import ctypes, sys
ERROR_ALREADY_EXISTS = 183
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
_mutex = kernel32.CreateMutexW(None, False, "Global\\MyAssistantSingleton") # 프로세스가 끝날 때까지 참조 유지
if ctypes.get_last_error() == ERROR_ALREADY_EXISTS:
print("이미 실행 중인 인스턴스가 있어 종료함")
sys.exit(0)
상황
음성 비서는 호출어 감지 → 음성을 글자로 바꾸는 STT → LLM(Claude Code CLI) 호출 → 음성 합성(TTS)이 모두 로컬 PC에서 도는 구조다. 응답은 claude -p를 자식 프로세스로 불러 만든다 . 파이썬은 venv 안의 pythonw.exe(콘솔 창이 없는 파이썬)로 상주한다 .
자동 시작 경로는 이랬다.
시작 프로그램 폴더의 VoiceAssistant.lnk
→ wscript.exe start_assistant_hidden.vbs (WshShell.Run, 창 스타일 0 = 숨김)
→ 배치 파일
→ pythonw.exe (로그는 배치에서 main_log.txt로 리다이렉트)
입력 장치는 USB 오디오이고, 앱은 시작할 때 장치 이름으로 장치를 찾는다 .
증상
- 가끔 뜨는 VBS 에러 창. "hidden_vbs 무슨 파일" 관련 에러 얼럿이 종종 떴다. 에러 문구는 기록하지 못했다 .
- 부팅 후 비서가 없다. 시작 프로그램에 바로가기는 그대로 있는데
pythonw.exe가 하나도 없었다 . 작업 관리자에서 비활성화된 것도 아니었다(레지스트리에 비활성 항목 없음) . - 로그가 조용하다. PC는 22:33에 부팅됐는데
main_log.txt는 그 뒤로 한 줄도 늘지 않았다. 죽은 게 아니라 시작 자체를 못 한 것처럼 보였다 . - 그런데 손으로 돌리면 멀쩡하다.
python.exe로 직접 실행하면 "준비 완료"까지 가고, 배치를 직접 돌려도 뜬다 .
원인
확정하지 못했다. 확인된 것과 추정을 나눠 적는다.
확인된 것:
- VBS 에러 창은 Windows Script Host가 스크립트 오류를 만나면 숨김 실행이어도 모달 창을 띄우기 때문에 보인다. 어떤 오류였는지는 문구가 없어 특정하지 못했다 .
- 부팅 실패는
wscript로 VBS를 돌리는 경로에서만 재현됐다. 4번 연속 모두 실패했고(pythonw 0개, 로그 0바이트 증가) , 같은 경로가 바로 다음엔 성공했다 . 실패할 때 로그가 0바이트였다는 건 파이썬 코드가 한 줄도 돌지 못했다는 뜻이다 . cscript(콘솔용 스크립트 호스트)로 돌려도 에러 없이 exit 0이었고,pythonw는 여전히 뜨지 않았다 . 다만 이때 VBS 맨 위의On Error Resume Next를 빼고 돌렸다는 기록은 없다. 그 한 줄이 남아 있으면cscript에서도 오류는 출력되지 않고 넘어가므로, 이 결과만으로 "삼킨 오류가 없었다"고 할 수는 없다. VBS가 끝까지 실행돼Run이 0을 반환한 것(Run returned: 0)은 성공한 회차에서 확인됐다 .- 창 스타일을 1(보임)로 바꾸면 떴고, 0(숨김)으로 되돌려도 그때는 떴다. 숨김 모드 자체가 원인이라고 할 수도 없었다 .
추정(검증 안 됨):
- 부팅 직후 Defender 실시간 검사, 시작 항목 스캔, USB 오디오 장치가 아직 열거되지 않은 상태와 겹친다는 정황 . 장치 이름을 못 찾으면 앱이
RuntimeError로 죽는 코드는 있었다 . 다만 실패 때 로그가 0바이트였으니 장치 단계까지 가지도 못했을 수 있어, 이 추정과 딱 맞지는 않는다.
주의할 점: 같은 날 새벽, 앞선 부팅에서 같은 증상을 조사할 때 조사에 쓰던 셸이 호출이 끝나면 자기가 띄운 자식 프로세스를 같이 정리해 버려 "떴다가 사라지는" 가짜 실패가 섞였다 . 셸 밖(WMI)으로 띄우자 멀쩡히 살아 있었다 . 위의 "4번 연속 실패"도 같은 종류의 도구 셸에서 돌린 것이라, 이 영향이 섞였을 가능성을 완전히 배제하지는 못한다. 자동화 도구나 원격 셸에서 재현 테스트를 한다면, 그 셸이 프로세스를 죽이는 건 아닌지 먼저 가려야 한다.
해결
원인을 모르는 채로 안정적인 쪽으로 실행 방식을 옮겼다.
전: 증상 숨기기 — 두 VBS 맨 위에 On Error Resume Next를 넣어 에러 창을 막았다 . 창은 사라졌지만 원인은 모른다. 며칠 뒤 부팅 실패를 쫓을 때 "이게 에러를 삼키고 있나"부터 의심해야 했고 , 그 한 줄이 남아 있는 한 cscript로 돌려도 오류를 볼 수 없었다.
후: 실행 방식 교체
- 작업 스케줄러
VoiceAssistantJarvis등록 — 로그온 45초 뒤, 실패 시 1분 간격 최대 3회 재시도, 중복 실행 무시. 시작 폴더 바로가기는 지우지 않고scripts/_disabled_startup/으로 옮겨 이중 실행을 막았다 . - 앱을 부팅에 강하게 — 시작할 때
[main] === 시작 <타임스탬프> (pid ...) ===를 찍는다. 다음에 또 안 뜨면 "시작 시도라도 했는지"를 로그만 보고 가릴 수 있다. 입력 스트림은 장치가 준비될 때까지 최대 60초 재시도한다 . 스케줄러로 실행하니 이 시작 로그가 실제로 찍혔다 . - 뮤텍스로 두 번째 인스턴스를 막았다. 일부러 하나 더 띄우니 "이미 실행 중인 인스턴스가 있어 종료함"을 찍고 바로 끝났다 .
- 처음엔 스케줄러가 배치를 실행하게 했는데 cmd 창이 계속 떠 있었다. 이 PC는 기본 터미널이 Windows 터미널이라 숨김 설정이 cmd 창에 먹지 않았다는 설명이었다. 스케줄러가 cmd 없이 파이썬을 바로 실행하게 하고, 로그 저장은 파이썬 코드 안으로 옮겼다. 대시보드 서버도 같은 방식으로 옮기고 시작 폴더 방식은 껐다 .
검증 범위: 작업을 즉시 실행(on-demand)해서 기동과 로그, 뮤텍스는 확인했다. 실제 재부팅으로 확인한 기록은 없다 . "기본 터미널이 Windows 터미널이라서"라는 설명도 따로 검증하지는 않았다.
덤 — pythonw가 두 개 떠 있는 건 중복 실행이 아니다
작업 관리자에서 앱마다 pythonw.exe가 두 개씩 보여 중복 실행을 의심했다. 부모 PID와 CPU를 보니 부모는 venv의 런처 스텁(CPU 0)이고 실제 일은 자식 하나(CPU 79.9%)가 했다 . 종료할 때는 PID 하나가 아니라 명령줄(CommandLine)로 걸러 둘 다 끈다 . 진짜 중복을 막고 싶으면 위의 뮤텍스를 쓴다.
취재 후기 — 에러 창을 끄고 나니 아무것도 안 보였다
pythonw가 하나도 안 떠 있어 main_log.txt는 그 뒤로 한 줄도 안 늘었어. 시작을 못 한 거야 On Error Resume Next 넣었잖아. 오류가 나도 무시하고 다음 줄로 넘어가라는 명령이야. 그게 진짜 에러를 삼키고 있는 거 아냐? wscript 대신 cscript로 돌려 보자. 같은 스크립트 호스트인데 에러를 창 대신 콘솔에 찍어 줘.cscript로 돌렸더니 에러 없이 exit 0이야. 그런데 pythonw는 여전히 안 떠 On Error Resume Next는 범인이 아니야.cscript에서도 조용히 넘어가. exit 0은 "오류가 없었다"일 수도 있고 "오류가 있었는데 안 보였다"일 수도 있어.cscript까지 꺼내 들고도 아무것도 못 본 이유가 그거야.wscript로 VBS를 여러 번 돌리고 성공률을 세 보자.wscript 경로가 범인이네.wscript로 떴어. 로그에 "준비 완료"까지 찍혔고 [main] === 시작 ... (pid 18320) ===이 찍혔어 며칠 뒤, 대시보드 서버까지 스케줄러로 옮긴 다음.
pythonw를 붙잡고 기다리는 거야 pythonw를 바로 실행하면 가설이 맞든 틀리든 창이 생길 자리가 없어. 로그는 파이썬 안에서 직접 쓰게 하고.On Error Resume Next는 에러 창을 끄는 스위치지 원인을 고치는 도구가 아니야. 원인을 못 잡겠으면 에러를 숨기지 말고, 실패해도 흔적이 남고 스스로 다시 일어나는 실행 방식으로 옮기는 게 답이야.