Merge branch 'develop' into release
This commit is contained in:
+36
-24
@@ -512,6 +512,38 @@ function tailscaleCommandEnv(loginServer: string): NodeJS.ProcessEnv | undefined
|
||||
return /^http:\/\//i.test(loginServer.trim()) ? { TS_DEBUG_ALLOW_HTTP: '1' } : undefined
|
||||
}
|
||||
|
||||
function getLauncherOsUser(): string {
|
||||
const user = (process.env.USER ?? process.env.LOGNAME ?? '').trim()
|
||||
return /^[a-z_][a-z0-9_-]*\$?$/i.test(user) ? user : ''
|
||||
}
|
||||
|
||||
async function ensureTailscaleOperator(): Promise<CommandResult> {
|
||||
const operator = getLauncherOsUser()
|
||||
if (!operator) {
|
||||
return { ok: false, message: '현재 OS 사용자를 확인하지 못했습니다.' }
|
||||
}
|
||||
|
||||
const args = ['set', `--operator=${operator}`]
|
||||
try {
|
||||
await runCommand('tailscale', args, 15000)
|
||||
return { ok: true, message: 'Tailscale 제어 권한이 준비되었습니다.' }
|
||||
} catch (error) {
|
||||
if (!isTailscalePermissionError(error)) {
|
||||
return { ok: false, message: commandErrorMessage(error, 'Tailscale 제어 권한 설정 실패') }
|
||||
}
|
||||
|
||||
try {
|
||||
await runCommand('pkexec', ['/usr/bin/tailscale', ...args], 30000)
|
||||
return { ok: true, message: 'Tailscale 제어 권한이 준비되었습니다.' }
|
||||
} catch (pkexecError) {
|
||||
return {
|
||||
ok: false,
|
||||
message: commandErrorMessage(pkexecError, commandErrorMessage(error, 'Tailscale 제어 권한 설정 실패')),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function installTailscale(): Promise<CommandResult> {
|
||||
const installScript = [
|
||||
'set -e',
|
||||
@@ -590,13 +622,6 @@ async function resetTailscaleSession(commandEnv?: NodeJS.ProcessEnv): Promise<vo
|
||||
await runCommandIfPossible('tailscale', ['down'], 10000, commandEnv)
|
||||
}
|
||||
|
||||
async function resetTailscaleSessionWithPkexec(commandEnv?: NodeJS.ProcessEnv): Promise<void> {
|
||||
const loggedOut = await runCommandIfPossible('pkexec', ['/usr/bin/tailscale', 'logout'], 30000, commandEnv)
|
||||
if (!loggedOut) {
|
||||
await runCommandIfPossible('pkexec', ['/usr/bin/tailscale', 'down'], 30000, commandEnv)
|
||||
}
|
||||
}
|
||||
|
||||
async function readTailscaleStatus(): Promise<TailscaleStatusResult> {
|
||||
const attempts: Array<[string, string[]]> = [
|
||||
['tailscale', ['status', '--json']],
|
||||
@@ -714,25 +739,11 @@ async function loginTailscale(
|
||||
await waitForTailscaleConnected()
|
||||
return { ok: true, message: 'Tailscale 연결 완료' }
|
||||
} catch (error) {
|
||||
if (isTailscalePermissionError(error)) {
|
||||
try {
|
||||
if (forceReset) {
|
||||
await resetTailscaleSessionWithPkexec(commandEnv)
|
||||
}
|
||||
await runCommand('pkexec', ['/usr/bin/tailscale', ...tailscaleArgs], 60000, commandEnv)
|
||||
await waitForTailscaleConnected()
|
||||
return { ok: true, message: 'Tailscale 연결 완료' }
|
||||
} catch (pkexecError) {
|
||||
return {
|
||||
ok: false,
|
||||
message: commandErrorMessage(pkexecError, commandErrorMessage(error, 'Tailscale 제어 권한이 없습니다.')),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
message: commandErrorMessage(error, 'Tailscale 연결 실패'),
|
||||
message: isTailscalePermissionError(error)
|
||||
? commandErrorMessage(error, 'Tailscale 제어 권한이 없습니다. 다시 연결해 주세요.')
|
||||
: commandErrorMessage(error, 'Tailscale 연결 실패'),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1027,6 +1038,7 @@ function createWindow(): void {
|
||||
})
|
||||
ipcMain.handle('launcher-tailscale-status', () => readTailscaleStatus())
|
||||
ipcMain.handle('launcher-tailscale-install', () => installTailscale())
|
||||
ipcMain.handle('launcher-tailscale-ensure-operator', () => ensureTailscaleOperator())
|
||||
ipcMain.handle('launcher-ssh-key-ensure', () => ensureLauncherSshKey())
|
||||
ipcMain.handle(
|
||||
'launcher-tailscale-login',
|
||||
|
||||
@@ -14,6 +14,7 @@ const appWindowMeta = { isAppWindow, appName, url: appUrl }
|
||||
const launcherTailscale = {
|
||||
status: () => ipcRenderer.invoke('launcher-tailscale-status'),
|
||||
install: () => ipcRenderer.invoke('launcher-tailscale-install'),
|
||||
ensureOperator: () => ipcRenderer.invoke('launcher-tailscale-ensure-operator'),
|
||||
login: (authKey: string, loginServer: string, hostname?: string, forceReset?: boolean) =>
|
||||
ipcRenderer.invoke('launcher-tailscale-login', authKey, loginServer, hostname, forceReset),
|
||||
logout: () => ipcRenderer.invoke('launcher-tailscale-logout'),
|
||||
|
||||
Vendored
+4
@@ -40,6 +40,10 @@ declare global {
|
||||
ok: boolean
|
||||
message: string
|
||||
}>
|
||||
ensureOperator: () => Promise<{
|
||||
ok: boolean
|
||||
message: string
|
||||
}>
|
||||
login: (authKey: string, loginServer: string, hostname?: string, forceReset?: boolean) => Promise<{
|
||||
ok: boolean
|
||||
message: string
|
||||
|
||||
@@ -24,6 +24,7 @@ const LMS_WEB_BASE_URL = `${(
|
||||
).replace(/\/+$/, '')}/`
|
||||
const NOTICE_READ_STORAGE_KEY = 'atnmy.notice.readSignatures'
|
||||
const LAST_LAUNCHED_APP_STORAGE_KEY = 'atnmy.launcher.lastLaunchedApp'
|
||||
const TAILSCALE_DEVICE_ID_STORAGE_KEY = 'atnmy.launcher.tailscale-device-id'
|
||||
const IS_DEV = import.meta.env.DEV
|
||||
const TAILSCALE_BACKGROUND_TIMEOUT_MS = 20_000
|
||||
|
||||
@@ -102,6 +103,21 @@ function isSameTailscaleAccount(statusAccount: string | undefined, loginAccount:
|
||||
return normalizeHeadscaleAccount(statusAccount) === normalizeHeadscaleAccount(loginAccount)
|
||||
}
|
||||
|
||||
function getTailscaleDeviceId(): string {
|
||||
try {
|
||||
const stored = window.localStorage.getItem(TAILSCALE_DEVICE_ID_STORAGE_KEY)?.trim()
|
||||
if (stored && /^[a-z0-9-]{6,24}$/i.test(stored)) return stored
|
||||
|
||||
const next = typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||
? crypto.randomUUID().replace(/-/g, '').slice(0, 10)
|
||||
: `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`
|
||||
window.localStorage.setItem(TAILSCALE_DEVICE_ID_STORAGE_KEY, next)
|
||||
return next
|
||||
} catch {
|
||||
return 'launcher-client'
|
||||
}
|
||||
}
|
||||
|
||||
function isTailscaleMissingStatus(status: Awaited<ReturnType<NonNullable<typeof window.launcherTailscale>['status']>> | null): boolean {
|
||||
return Boolean(
|
||||
status?.backendState === 'Unavailable' &&
|
||||
@@ -272,11 +288,24 @@ function Dashboard({ authSession, onSessionUpdate, onLogout, onSessionExpired }:
|
||||
const sshKey = await window.launcherSshKey?.ensure().catch(() => null)
|
||||
if (cancelled) return
|
||||
|
||||
const operator = await launcherTailscale.ensureOperator()
|
||||
if (cancelled) return
|
||||
if (!operator.ok) {
|
||||
setTailscaleIssue({
|
||||
state: 'warning',
|
||||
title: '원격 연결 권한 필요',
|
||||
message: operator.message || 'Tailscale 제어 권한을 설정하지 못했습니다.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const response = await fetchWithAuthRefreshRef.current(`${LAUNCHER_API_BASE_URL}/api/provision/session`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
hostname: window.navigator.userAgent.includes('Electron') ? 'launcher-client' : 'client',
|
||||
hostname: window.navigator.userAgent.includes('Electron')
|
||||
? `launcher-client-${getTailscaleDeviceId()}`
|
||||
: `client-${getTailscaleDeviceId()}`,
|
||||
usageType: 'launcher-client',
|
||||
publicSshKey: sshKey?.publicKey,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user