fix: isolate gateway profile environment (#745)
This commit is contained in:
@@ -107,6 +107,73 @@ const needsRunMode = true
|
|||||||
// 启动时输出 init 系统检测结果(方便调试)
|
// 启动时输出 init 系统检测结果(方便调试)
|
||||||
logger.debug('Detected init system: %s (needsRunMode: %s, platform: %s)', initSystem, needsRunMode, process.platform)
|
logger.debug('Detected init system: %s (needsRunMode: %s, platform: %s)', initSystem, needsRunMode, process.platform)
|
||||||
|
|
||||||
|
const GATEWAY_RUNTIME_ENV_KEYS = new Set([
|
||||||
|
'PATH',
|
||||||
|
'HOME',
|
||||||
|
'USER',
|
||||||
|
'USERNAME',
|
||||||
|
'SHELL',
|
||||||
|
'LANG',
|
||||||
|
'TZ',
|
||||||
|
'TMP',
|
||||||
|
'TEMP',
|
||||||
|
'TMPDIR',
|
||||||
|
'HTTP_PROXY',
|
||||||
|
'HTTPS_PROXY',
|
||||||
|
'ALL_PROXY',
|
||||||
|
'NO_PROXY',
|
||||||
|
'HERMES_BIN',
|
||||||
|
'HERMES_ALLOW_ROOT_GATEWAY',
|
||||||
|
'SYSTEMROOT',
|
||||||
|
'COMSPEC',
|
||||||
|
'APPDATA',
|
||||||
|
'LOCALAPPDATA',
|
||||||
|
])
|
||||||
|
|
||||||
|
function parseEnvKeys(raw: string): Set<string> {
|
||||||
|
const keys = new Set<string>()
|
||||||
|
for (const line of raw.split(/\r?\n/)) {
|
||||||
|
const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)
|
||||||
|
if (match) keys.add(match[1])
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
function readProfileEnvKeys(): Set<string> {
|
||||||
|
const keys = new Set<string>()
|
||||||
|
const envPaths = [join(HERMES_BASE, '.env')]
|
||||||
|
const profilesDir = join(HERMES_BASE, 'profiles')
|
||||||
|
|
||||||
|
if (existsSync(profilesDir)) {
|
||||||
|
for (const entry of readdirSync(profilesDir, { withFileTypes: true })) {
|
||||||
|
if (entry.isDirectory()) envPaths.push(join(profilesDir, entry.name, '.env'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const envPath of envPaths) {
|
||||||
|
try {
|
||||||
|
for (const key of parseEnvKeys(readFileSync(envPath, 'utf-8'))) keys.add(key)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildGatewayProcessEnv(profileName: string, hermesHome: string): NodeJS.ProcessEnv {
|
||||||
|
const base = { ...process.env }
|
||||||
|
|
||||||
|
if (profileName !== 'default') {
|
||||||
|
for (const key of readProfileEnvKeys()) {
|
||||||
|
if (!GATEWAY_RUNTIME_ENV_KEYS.has(key.toUpperCase())) delete base[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
HERMES_HOME: hermesHome,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// 类型定义
|
// 类型定义
|
||||||
// ============================
|
// ============================
|
||||||
@@ -577,7 +644,7 @@ export class GatewayManager {
|
|||||||
|
|
||||||
// 所有平台统一使用 run 模式;dev/nodemon 可通过 env 保留 gateway 进程。
|
// 所有平台统一使用 run 模式;dev/nodemon 可通过 env 保留 gateway 进程。
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const env = { ...process.env, HERMES_HOME: hermesHome }
|
const env = buildGatewayProcessEnv(name, hermesHome)
|
||||||
const detachGateway = shouldDetachGatewayProcess()
|
const detachGateway = shouldDetachGatewayProcess()
|
||||||
const child = spawn(HERMES_BIN, ['gateway', 'run', '--replace'], {
|
const child = spawn(HERMES_BIN, ['gateway', 'run', '--replace'], {
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
@@ -689,7 +756,7 @@ export class GatewayManager {
|
|||||||
const { stdout, stderr } = await execFileAsync(HERMES_BIN, ['gateway', 'stop'], {
|
const { stdout, stderr } = await execFileAsync(HERMES_BIN, ['gateway', 'stop'], {
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
env: { ...process.env, HERMES_HOME: hermesHome },
|
env: buildGatewayProcessEnv(name, hermesHome),
|
||||||
})
|
})
|
||||||
const output = `${stdout}${stderr}`.trim()
|
const output = `${stdout}${stderr}`.trim()
|
||||||
if (output) logger.debug('%s: hermes gateway stop: %s', name, output)
|
if (output) logger.debug('%s: hermes gateway stop: %s', name, output)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { join } from 'path'
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
const originalHermesHome = process.env.HERMES_HOME
|
const originalHermesHome = process.env.HERMES_HOME
|
||||||
|
const originalEnv = { ...process.env }
|
||||||
const tempHomes: string[] = []
|
const tempHomes: string[] = []
|
||||||
|
|
||||||
function createHermesHome(): string {
|
function createHermesHome(): string {
|
||||||
@@ -22,6 +23,7 @@ async function createManager(home: string): Promise<any> {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks()
|
vi.restoreAllMocks()
|
||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
|
process.env = { ...originalEnv }
|
||||||
if (originalHermesHome === undefined) {
|
if (originalHermesHome === undefined) {
|
||||||
delete process.env.HERMES_HOME
|
delete process.env.HERMES_HOME
|
||||||
} else {
|
} else {
|
||||||
@@ -95,3 +97,76 @@ describe('GatewayManager Windows process recovery', () => {
|
|||||||
expect(manager.readPidFile('work')).toBe(33333)
|
expect(manager.readPidFile('work')).toBe(33333)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('GatewayManager gateway process env', () => {
|
||||||
|
it('keeps full inherited env for the default profile for compatibility', async () => {
|
||||||
|
const home = createHermesHome()
|
||||||
|
process.env.WEIXIN_TOKEN = 'from-parent'
|
||||||
|
process.env.CUSTOM_GATEWAY_SETTING = 'keep-me'
|
||||||
|
process.env.HERMES_HOME = home
|
||||||
|
vi.resetModules()
|
||||||
|
const { buildGatewayProcessEnv } = await import('../../packages/server/src/services/hermes/gateway-manager')
|
||||||
|
|
||||||
|
const env = buildGatewayProcessEnv('default', home)
|
||||||
|
|
||||||
|
expect(env.WEIXIN_TOKEN).toBe('from-parent')
|
||||||
|
expect(env.CUSTOM_GATEWAY_SETTING).toBe('keep-me')
|
||||||
|
expect(env.HERMES_HOME).toBe(home)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes parent env keys defined by any profile env for non-default profile gateways', async () => {
|
||||||
|
const home = createHermesHome()
|
||||||
|
const workHome = join(home, 'profiles', 'work')
|
||||||
|
mkdirSync(workHome, { recursive: true })
|
||||||
|
writeFileSync(join(home, '.env'), [
|
||||||
|
'WEIXIN_TOKEN=default-weixin',
|
||||||
|
'WECOM_SECRET=default-wecom',
|
||||||
|
'FUTURE_PLATFORM_TOKEN=default-future',
|
||||||
|
'export EXPORTED_SECRET=default-export',
|
||||||
|
'PATH=/default/path',
|
||||||
|
'HTTP_PROXY=http://default-proxy.local:8080',
|
||||||
|
'COMMENTED_OUT_SECRET=not-commented',
|
||||||
|
'# COMMENTED_OUT_SECRET=commented',
|
||||||
|
].join('\n'))
|
||||||
|
writeFileSync(join(workHome, '.env'), [
|
||||||
|
'WORK_ONLY_TOKEN=work-profile',
|
||||||
|
'PARENT_OVERRIDE_ME=work-profile',
|
||||||
|
].join('\n'))
|
||||||
|
|
||||||
|
process.env.PATH = '/opt/hermes/.venv/bin:/usr/bin'
|
||||||
|
process.env.HOME = '/home/agent'
|
||||||
|
process.env.HTTP_PROXY = 'http://proxy.local:8080'
|
||||||
|
process.env.HERMES_BIN = '/opt/hermes/.venv/bin/hermes'
|
||||||
|
process.env.HERMES_ALLOW_ROOT_GATEWAY = '1'
|
||||||
|
process.env.HERMES_HOME = home
|
||||||
|
process.env.WEIXIN_TOKEN = 'from-parent'
|
||||||
|
process.env.WECOM_SECRET = 'from-parent'
|
||||||
|
process.env.FUTURE_PLATFORM_TOKEN = 'from-parent'
|
||||||
|
process.env.EXPORTED_SECRET = 'from-parent'
|
||||||
|
process.env.WORK_ONLY_TOKEN = 'from-parent'
|
||||||
|
process.env.PARENT_OVERRIDE_ME = 'from-parent'
|
||||||
|
process.env.UNKNOWN_SERVICE_TOKEN = 'keep-me'
|
||||||
|
process.env.COMMENTED_OUT_SECRET = 'from-parent'
|
||||||
|
process.env.CUSTOM_GATEWAY_SETTING = 'from-parent'
|
||||||
|
vi.resetModules()
|
||||||
|
const { buildGatewayProcessEnv } = await import('../../packages/server/src/services/hermes/gateway-manager')
|
||||||
|
|
||||||
|
const env = buildGatewayProcessEnv('work', join(home, 'profiles', 'work'))
|
||||||
|
|
||||||
|
expect(env.HERMES_HOME).toBe(join(home, 'profiles', 'work'))
|
||||||
|
expect(env.PATH).toBe('/opt/hermes/.venv/bin:/usr/bin')
|
||||||
|
expect(env.HOME).toBe('/home/agent')
|
||||||
|
expect(env.HTTP_PROXY).toBe('http://proxy.local:8080')
|
||||||
|
expect(env.HERMES_BIN).toBe('/opt/hermes/.venv/bin/hermes')
|
||||||
|
expect(env.HERMES_ALLOW_ROOT_GATEWAY).toBe('1')
|
||||||
|
expect(env.WEIXIN_TOKEN).toBeUndefined()
|
||||||
|
expect(env.WECOM_SECRET).toBeUndefined()
|
||||||
|
expect(env.FUTURE_PLATFORM_TOKEN).toBeUndefined()
|
||||||
|
expect(env.EXPORTED_SECRET).toBeUndefined()
|
||||||
|
expect(env.WORK_ONLY_TOKEN).toBeUndefined()
|
||||||
|
expect(env.PARENT_OVERRIDE_ME).toBeUndefined()
|
||||||
|
expect(env.COMMENTED_OUT_SECRET).toBeUndefined()
|
||||||
|
expect(env.UNKNOWN_SERVICE_TOKEN).toBe('keep-me')
|
||||||
|
expect(env.CUSTOM_GATEWAY_SETTING).toBe('from-parent')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user