installer: run.bat 에 서버 기동/종료시 UPnP 자동 등록·해제 주입
서버가 실행 중일 때만 25565(또는 server-port) 가 열려 있도록 run.bat 을 후처리해 java 호출 전 UPnP Add, 종료 후 UPnP Remove 를 PowerShell 한 줄 (HNetCfg.NATUPnP.1)로 끼워 넣는다. Add 전에 같은 포트 매핑을 Remove 하므로 재실행에도 idempotent. 포트체크 단계에서 만든 테스트용 UPnP 매핑은 테스트 직후 제거해 실제 개방은 run.bat 이 단독으로 책임지게 한다. 제한: 콘솔창 X 강제 종료 시 teardown 미실행. 라우터 TTL 만료 또는 다음 실행 시 재등록 직전 Remove 로 자연 정리.
This commit is contained in:
@@ -298,8 +298,82 @@ 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 자동 등록/해제 로직 주입.
|
||||||
|
// 이렇게 해야 서버가 안 떠 있는 동안에는 포트가 닫혀 있게 된다.
|
||||||
|
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('run.bat 이 없어 UPnP 자동 등록 스크립트 주입을 건너뜁니다.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const MARKER = 'REM === UPNP MANAGED BY MUSICQUIZ INSTALLER ==='
|
||||||
|
const original = await fsp.readFile(runBat, 'utf8')
|
||||||
|
if (original.includes(MARKER)) {
|
||||||
|
sendLog('run.bat 에 이미 UPnP 자동 등록 스크립트가 들어 있어 건너뜁니다.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = original.split(/\r?\n/)
|
||||||
|
const javaIdx = lines.findIndex((line) => /^\s*java(\.exe)?[\s"]/i.test(line))
|
||||||
|
if (javaIdx === -1) {
|
||||||
|
sendLog('run.bat 에서 java 호출 라인을 찾지 못해 UPnP 자동 등록 주입을 건너뜁니다.')
|
||||||
|
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('run.bat 에 서버 기동/종료 시 UPnP 자동 등록·해제 스크립트를 추가했습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
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')
|
||||||
@@ -529,11 +603,16 @@ ipcMain.handle('server:portForward', async (_event, port: number): Promise<PortF
|
|||||||
probe = await probePortFromOutside(targetPort, externalIp)
|
probe = await probePortFromOutside(targetPort, externalIp)
|
||||||
if (!externalIp && probe.detectedIp) externalIp = probe.detectedIp
|
if (!externalIp && probe.detectedIp) externalIp = probe.detectedIp
|
||||||
if (probe.reachable === true) {
|
if (probe.reachable === true) {
|
||||||
sendLog(`UPnP로 포트 ${targetPort} 자동 개방 완료.`)
|
sendLog(`UPnP로 포트 ${targetPort} 자동 개방 완료. 테스트 매핑을 제거합니다(실제 개방은 run.bat 이 서버 기동 시 자동으로 처리).`)
|
||||||
|
await removeUpnpMapping(targetPort)
|
||||||
return { status: 'upnpOk', externalIp, port: targetPort }
|
return { status: 'upnpOk', externalIp, port: targetPort }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 테스트 목적으로 만든 매핑 정리. 실제 개방은 run.bat 이 담당.
|
||||||
|
sendLog('테스트용 UPnP 매핑을 정리합니다.')
|
||||||
|
await removeUpnpMapping(targetPort)
|
||||||
|
|
||||||
const reason = probe.reachable === false
|
const reason = probe.reachable === false
|
||||||
? 'UPnP 매핑은 등록됐지만 외부 포트체크 서비스에서 연결이 닿지 않았습니다. ISP 차단, 이중 NAT, 또는 방화벽 설정을 확인하세요.'
|
? 'UPnP 매핑은 등록됐지만 외부 포트체크 서비스에서 연결이 닿지 않았습니다. ISP 차단, 이중 NAT, 또는 방화벽 설정을 확인하세요.'
|
||||||
: `외부 포트체크 결과를 받지 못했습니다(${probe.detail}). UPnP 매핑은 등록됐을 수 있습니다.`
|
: `외부 포트체크 결과를 받지 못했습니다(${probe.detail}). UPnP 매핑은 등록됐을 수 있습니다.`
|
||||||
|
|||||||
Reference in New Issue
Block a user