8000 fix: 올리브영 브라우저 준비 후 자동 숨김 by hmmhmmhm · Pull Request #320 · hmmhmmhm/daiso-mcp · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/macos-ops-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/page.aka.daiso-op
launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/page.aka.daiso-ops-monitor.plist"
```

업데이트할 때는 새 plist 설치 후 이 두 보조 작업만 `bootout`하고 다시 `bootstrap`한다. 중계·터널 작업의 종료나 재시작은 포함하지 않는다. 숨김 도우미는 비공개 `state/browser-owner.json`의 PID·부모 PID·생성 시각과 실제 프로세스 UID·실행 파일이 모두 맞을 때만 숨긴다. 이미 숨겨진 앱도 소유권을 검증한다. 사용자 Chrome이나 소유권이 불확실한 프로세스에는 요청하지 않는다.
업데이트할 때는 새 plist 설치 후 이 두 보조 작업만 `bootout`하고 다시 `bootstrap`한다. 중계·터널 작업의 종료나 재시작은 포함하지 않는다. 숨김 도우미는 비공개 `state/browser-owner.json`의 PID·부모 PID·생성 시각과 실제 프로세스 UID·실행 파일이 모두 맞을 때만 숨긴다. 이미 숨겨진 앱도 소유권을 검증한다. 새 브라우저 생성 시각 이후의 로컬 감시 기록이 `ready`를 확인한 다음 숨긴다. 초기 브라우저 생성과 숨김이 겹치지 않도록 이전 세션의 기록, 3분 넘게 오래된 기록, 준비 중 또는 실패한 기록은 사용하지 않는다. 감시 기록의 시각은 마지막 저장 시각이 아니라 조회 시작 시각을 사용해, 점검 도중 교체된 새 브라우저에 이전 준비 결과가 적용되지 않게 한다. 로컬 감시는 60초 주기이므로 준비 완료 뒤 숨김까지 최대 한 주기 지연될 수 있다. 사용자 Chrome이나 소유권이 불확실한 프로세스에는 요청하지 않는다.

## 감시와 상태 확인

Expand Down
28 changes: 26 additions & 2 deletions scripts/relay/hide-oliveyoung.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ func argument(_ name: String, fallback: URL) -> URL {
CommandLine.arguments[index + 1].hasPrefix("/") else { exit(2) }
return URL(fileURLWithPath: CommandLine.arguments[index + 1]).standardizedFileURL
}
// 새 프로세스 준비 전의 이전 스냅샷이나 오래된 상태로 숨기지 않습니다.
func readinessAllowsHiding(_ value: [String: Any], launchedAt: TimeInterval, now: TimeInterval) -> Bool {
guard let checkedAt = value["checkedAt"] as? Double,
checkedAt >= launchedAt + 1, checkedAt <= now + 5, now - checkedAt <= 180,
let checks = value["checks"] as? [String: Any],
let oliveyoung = checks["oliveyoung"] as? [String: Any],
oliveyoung["state"] as? String == "ready", oliveyoung["failed"] as? Bool == false else { return false }
return true
}

let base = argument("--state-base", fallback: FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support/DaisoRelay"))
let runtime = argument("--runtime", fallback: base.appendingPathComponent("runtime"))
let marker = base.appendingPathComponent("state/browser-owner.json")
Expand All @@ -22,6 +32,20 @@ func safeMarkerPath() -> Bool {
return true
}

func readyForHide(_ app: NSRunningApplication) -> Bool {
let status = base.appendingPathComponent("ops-status.json")
guard let launched = app.launchDate,
status.resolvingSymlinksInPath().path == status.path,
let attributes = try? FileManager.default.attributesOfItem(atPath: status.path),
attributes[.type] as? FileAttributeType == .typeRegular,
attributes[.ownerAccountID] as? UInt32 == getuid(),
let mode = attributes[.posixPermissions] as? UInt16, mode & 0o077 == 0,
let size 8000 = attributes[.size] as? UInt64, size <= 65536,
let data = try? Data(contentsOf: status),
let value = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return false }
return readinessAllowsHiding(value, launchedAt: launched.timeIntervalSince1970, now: Date().timeIntervalSince1970)
}

// 소유 마커가 바뀌거나 검증에 실패하면 숨김을 생략한다.
func target() -> NSRunningApplication? {
guard safeMarkerPath(), let attributes = try? FileManager.default.attributesOfItem(atPath: marker.path),
Expand Down Expand Up @@ -57,13 +81,13 @@ func target() -> NSRunningApplication? {

// 매초 확인하되 이미 숨겨진 앱에는 추가 UI 요청을 보내지 않는다.
func hideOwnedBrowser() {
guard let app = target(), !app.isHidden else { return }
guard let app = target(), !app.isHidden, readyForHide(app) else { return }
_ = app.hide()
}

if CommandLine.arguments.contains("--check") {
if let app = target() {
print("owned=true hidden=\(app.isHidden) pid=\(app.processIdentifier)")
print("owned=true hidden=\(app.isHidden) pid=\(app.processIdentifier) readyToHide=\(readyForHide(app))")
} else {
print("owned=false")
exit(1)
Expand Down
2 changes: 1 addition & 1 deletion scripts/relay/macos-ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ def login_notification(checks, previous, now, enabled):


def monitor(base, tunnel_port, notify_local=False):
checked_at = time.time()
checks = {}
for name, env_file, token_key, port, path in [
('oliveyoung', 'relay.env', 'OY_RELAY_TOKEN', 4319, '/health'),
Expand Down Expand Up @@ -248,7 +249,6 @@ def monitor(base, tunnel_port, notify_local=False):
checks['tunnel'] = {'failed': True, 'reason': 'readiness-unavailable'}
previous_status = snapshot_status(base / 'ops-status.json')
previous = previous_status.get('checks', {})
checked_at = time.time()
for name, check in checks.items():
old = previous.get(name, {})
if check['failed']:
Expand Down
15 changes: 15 additions & 0 deletions tests/scripts/fixtures/macos_ops_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ def fetch(port,path,token=None):
self.assertEqual(second['checks']['oliveyoung']['consecutiveFailures'],2)
self.assertEqual(first['checks']['oliveyoung']['firstFailedAt'], second['checks']['oliveyoung']['firstFailedAt'])

def test_monitor_timestamps_observation_before_later_checks(self):
with tempfile.TemporaryDirectory() as root:
base=Path(root).resolve()
for name in ['relay.env', 'dtryx.env']:
(base/name).write_text('OY_RELAY_TOKEN=secret\nDTRYX_RELAY_TOKEN=secret\nCONVENIENCE_RELAY_TOKEN=secret\n')
(base/name).chmod(0o600)
clock=[1000]
def fetch(port,path,token=None):
if path != '/health': clock[0]=1200
return {'state':'ready'}
with patch.object(ops.time,'time',side_effect=lambda: clock[0]), patch.object(ops,'fetch_health',side_effect=fetch):
result=ops.monitor(base,0)
self.assertEqual(result['checkedAt'],1000)
self.assertEqual(clock[0],1200)

def test_configured_gs_session_requires_observable_authentication(self):
with tempfile.TemporaryDirectory() as root:
base=Path(root).resolve()
Expand Down
44 changes: 44 additions & 0 deletions tests/scripts/hide-readiness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { execFileSync } from 'node:child_process';
import { readFileSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { expect, it } from 'vitest';
it.skipIf(process.platform !== 'darwin')(
'새 브라우저 생성 이후의 유효한 준비 완료 기록이 있을 때만 숨김을 허용한다',
() => {
const source = readFileSync('scripts/relay/hide-oliveyoung.swift', 'utf8');
expect(source).toContain('func readinessAllowsHiding');
const start = source.indexOf('func readinessAllowsHiding');
const end = source.indexOf('\nlet base =', start);
const implementation = source.slice(start, end);
const ready = { checkedAt: 1100, checks: { oliveyoung: { state: 'ready', failed: false } } };
const cases = [
ready,
{ ...ready, checkedAt: 999 },
{ ...ready, checkedAt: 1000.5 },
{ ...ready, checkedAt: 1010 },
{ ...ready, checkedAt: 1206 },
{ ...ready, checkedAt: '1100' },
{ ...ready, checks: { oliveyoung: { state: 'starting', failed: false } } },
{ ...ready, checks: { oliveyoung: { state: 'ready', failed: true } } },
{ ...ready, checks: {} },
{},
];
const directory = mkdtempSync(join(tmpdir(), 'hide-readiness-'));
try {
const script = join(directory, 'main.swift');
const data = join(directory, 'cases.json');
writeFileSync(data, JSON.stringify(cases));
writeFileSync(
script,
'import Foundation\n' +
implementation +
'\nlet cases=try JSONSerialization.jsonObject(with:Data(contentsOf:URL(fileURLWithPath:CommandLine.arguments[1]))) as! [[String:Any]]\nfor row in cases { print(readinessAllowsHiding(row, launchedAt:1000, now:1200)) }\n',
);
const output = execFileSync('/usr/bin/swift', [script, data], { encoding: 'utf8' });
expect(output.trim().split('\n')).toEqual(['true', ...Array(9).fill('false')]);
} finally {
rmSync(directory, { recursive: true, force: true });
}
},
);
Loading
0