1 Commits

Author SHA1 Message Date
2fa1173b8e installer: stop injecting UPnP auto-open into run.bat (check-only)
리뷰 반영. 포트포워딩 페이지를 check-only 로 바꾼 것과 정합성을 맞춰, 서버 설치
시 run.bat 에 UPnP 자동 등록/해제를 주입하던 로직(injectUpnpToRunBat)을 제거.
이제 설치기는 포트를 직접 열지 않고, 사용자가 수동 포워딩 후 페이지에서 확인만
한다. v0.3.21.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-07-11 00:50:37 +09:00
2 changed files with 3 additions and 76 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "minecraft-music-quiz-installer", "name": "minecraft-music-quiz-installer",
"version": "0.3.20", "version": "0.3.21",
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
"main": "dist/installer/main.js", "main": "dist/installer/main.js",
"scripts": { "scripts": {

View File

@@ -617,83 +617,10 @@ ipcMain.handle('server:install', async (_event, payload: ServerInstallPayload) =
await downloadServerZip(pack.pack, installPath) await downloadServerZip(pack.pack, installPath)
// 다운로드한 zip에 들어있을 수 있는 eula.txt를 그대로 보존한다. // 다운로드한 zip에 들어있을 수 있는 eula.txt를 그대로 보존한다.
// 동의 흐름은 renderer가 별도 IPC로 읽고 동의 시 덮어쓴다. // 동의 흐름은 renderer가 별도 IPC로 읽고 동의 시 덮어쓴다.
// 설치기는 포트를 직접 열지 않는다(요청). run.bat 에 UPnP 자동 개방을 주입하지
// run.bat 에 서버 기동/종료시 UPnP 자동 등록/해제 로직 주입. // 않으며, 사용자가 라우터에서 수동 포워딩을 하고 포트포워딩 페이지에서 확인만 한다.
// 이렇게 해야 서버가 안 떠 있는 동안에는 포트가 닫혀 있게 된다.
await injectUpnpToRunBat(installPath)
}) })
/**
* 추출된 서버 zip 의 run.bat 에 UPnP 자동 등록(서버 시작 시) / 자동 해제(서버 종료 후)
* 스크립트를 끼워 넣는다. 이미 우리가 주입했던 마커가 있으면 다시 건드리지 않는다.
*
* 동작:
* 1) 서버 시작 직전: server.properties 의 server-port 값(없으면 25565) 으로 PowerShell
* 을 통해 HNetCfg.NATUPnP.1 COM 객체를 이용해 정적 포트 매핑 추가.
* 2) 서버 프로세스 종료 후(=pause 직전 또는 파일 끝): 동일한 포트의 매핑 제거.
*
* 제한: 사용자가 콘솔 창을 X 버튼으로 강제 종료하면 teardown 이 실행되지 않는다.
* 이 경우 라우터의 UPnP TTL 에 의해 자동 만료되며, 다음 실행 시 Add 전에 Remove 를
* 시도하므로 idempotent.
*/
async function injectUpnpToRunBat(installPath: string): Promise<void> {
const runBat = path.join(installPath, 'run.bat')
if (!fs.existsSync(runBat)) {
sendLog(t('log.runBatMissing'))
return
}
const MARKER = 'REM === UPNP MANAGED BY MUSICQUIZ INSTALLER ==='
const original = await fsp.readFile(runBat, 'utf8')
if (original.includes(MARKER)) {
sendLog(t('log.runBatAlreadyInjected'))
return
}
const lines = original.split(/\r?\n/)
const javaIdx = lines.findIndex((line) => /^\s*java(\.exe)?[\s"]/i.test(line))
if (javaIdx === -1) {
sendLog(t('log.runBatNoJava'))
return
}
let pauseIdx = -1
for (let i = javaIdx + 1; i < lines.length; i++) {
if (/^\s*pause\b/i.test(lines[i])) { pauseIdx = i; break }
}
if (pauseIdx === -1) pauseIdx = lines.length
// PowerShell 한 줄로 처리: server.properties 의 server-port 우선, 없으면 25565.
// Add 전에 같은 포트의 매핑이 남아 있으면 먼저 Remove 하여 idempotent 하게 만든다.
const addBlock = [
MARKER,
'REM 서버 시작 직전: server-port 추출 후 UPnP 매핑 등록.',
'set "_MQ_PORT=25565"',
'for /f "tokens=2 delims==" %%a in (\'findstr /b /c:"server-port=" server.properties 2^>nul\') do set "_MQ_PORT=%%a"',
'set "_MQ_PORT=%_MQ_PORT: =%"',
'echo [MusicQuiz] UPnP 등록 시도: TCP %_MQ_PORT%',
'powershell -NoProfile -Command "$port=[int]$env:_MQ_PORT; $ip=(Get-NetIPAddress -AddressFamily IPv4 -PrefixOrigin Dhcp,Manual -ErrorAction SilentlyContinue | Where-Object {$_.IPAddress -notlike \'169.254.*\' -and $_.IPAddress -ne \'127.0.0.1\'} | Select-Object -First 1).IPAddress; if (-not $ip) { Write-Host \'[MusicQuiz] 로컬 IPv4 검색 실패\'; exit 1 }; try { $u = New-Object -ComObject HNetCfg.NATUPnP.1; $c=$u.StaticPortMappingCollection; if ($c) { try { $c.Remove($port,\'TCP\') | Out-Null } catch {}; $c.Add($port,\'TCP\',$port,$ip,$true,\'MusicQuiz Minecraft Server\') | Out-Null; Write-Host (\'[MusicQuiz] UPnP 등록 성공: \' + $ip + \':\' + $port + \' TCP\') } else { Write-Host \'[MusicQuiz] UPnP 컬렉션 사용 불가(라우터 UPnP 꺼짐?)\' } } catch { Write-Host (\'[MusicQuiz] UPnP 등록 실패: \' + $_.Exception.Message) }"'
]
const removeBlock = [
'REM 서버 종료 후: UPnP 매핑 해제.',
'echo [MusicQuiz] UPnP 해제 시도: TCP %_MQ_PORT%',
'powershell -NoProfile -Command "$port=[int]$env:_MQ_PORT; try { $u = New-Object -ComObject HNetCfg.NATUPnP.1; $c=$u.StaticPortMappingCollection; if ($c) { $c.Remove($port,\'TCP\') | Out-Null; Write-Host (\'[MusicQuiz] UPnP 해제 완료: TCP \' + $port) } } catch { Write-Host (\'[MusicQuiz] UPnP 해제 실패: \' + $_.Exception.Message) }"'
]
const merged: string[] = []
merged.push(...lines.slice(0, javaIdx))
merged.push(...addBlock)
merged.push(lines[javaIdx])
merged.push(...lines.slice(javaIdx + 1, pauseIdx))
merged.push(...removeBlock)
merged.push(...lines.slice(pauseIdx))
// bat 파일은 CRLF 가 안전.
const output = merged.join('\r\n')
await fsp.writeFile(runBat, output, 'utf8')
sendLog(t('log.runBatInjected'))
}
ipcMain.handle('server:readEula', async (_event, installPath: string): Promise<{ exists: boolean; content: string }> => { ipcMain.handle('server:readEula', async (_event, installPath: string): Promise<{ exists: boolean; content: string }> => {
if (!installPath) return { exists: false, content: '' } if (!installPath) return { exists: false, content: '' }
const target = path.join(path.resolve(installPath), 'eula.txt') const target = path.join(path.resolve(installPath), 'eula.txt')