From d72f5632d5fa0b08dd37b7bc1517d6f2f2f8e715 Mon Sep 17 00:00:00 2001
From: maze-mei
Date: Sat, 22 Aug 2026 21:50:04 +0900
Subject: [PATCH 1/6] Document v2.0.0 handoff and next work
---
AGENTS.md | 14 +--
docs/AI_WORKFLOW.md | 3 +-
docs/HANDOFF.md | 273 ++++++++++++++++++++++++++++++++++++++++++
docs/PROJECT_BRIEF.md | 11 +-
docs/README.md | 5 +-
docs/SESSION.md | 28 ++++-
docs/TODO.md | 29 ++++-
7 files changed, 343 insertions(+), 20 deletions(-)
create mode 100644 docs/HANDOFF.md
diff --git a/AGENTS.md b/AGENTS.md
index 0125bae..1aa2706 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,8 +6,8 @@
## 작업 시작
-1. `docs/README.md`, `docs/PROJECT_BRIEF.md`, `docs/SESSION.md`,
- `docs/TODO.md`를 읽는다.
+1. `docs/README.md`, `docs/HANDOFF.md`, `docs/PROJECT_BRIEF.md`,
+ `docs/SESSION.md`, `docs/TODO.md`를 읽는다.
2. Git 상태와 최근 커밋을 확인하고 사용자 변경 및 미커밋 파일을 보존한다.
3. 설계 이유가 필요한 작업은 `docs/DECISIONS.md`, 구현 세부 사항은
`docs/TECH_NOTES.md`, 알려진 제약은 `docs/KNOWN_ISSUES.md`를 확인한다.
@@ -39,8 +39,8 @@
적용한다. DX Manager 자체의 장치 관리, 무선 연결, wake-up과 화면 상태
명령 경로를 가로채지 않는다.
- Scrcpy 시작 직렬화와 프로세스 정리 규칙을 유지한다.
-- v1 계열은 최초 선택한 물리 기기를 프로그램 종료까지 관리한다. 같은 기기의
- USB/무선 transport 전환은 허용하지만 다른 물리 기기로 자동 전환하지 않는다.
+- 현재 v2 계열은 여러 물리 기기를 동시에 관리한다. 모든 장치 명령과 정리 범위는
+ 물리 identity별 런타임 및 작업 시작 시 캡처한 명시적 serial로 제한한다.
- display ID는 실제 설정과 디스플레이 목록의 전후 차이로 찾으며 모호한 ID를
추측하지 않는다.
- DeX overlay는 너비, 높이와 DPI가 모두 같을 때만 재사용한다. 하나라도 다르면
@@ -56,10 +56,10 @@
- 백그라운드 작업 결과로 UI를 갱신할 때 WinForms UI 스레드 규칙과 폼 종료
경합을 고려한다.
-## 복수 휴대폰 선행 설계
+## 복수 휴대폰 v2 불변 조건
-- v1 계열의 사용자 동작은 한 대의 물리 기기 고정을 유지하되, 새 기능과 기존
- 기능의 구조 개선은 v2의 복수 휴대폰 동시 지원을 기본 전제로 설계한다.
+- 공개 v2.0.0의 복수 휴대폰 동시 지원을 모든 새 기능과 구조 개선의 기본 전제로
+ 유지한다. v1의 최초 기기 고정은 과거 버전 동작이며 현재 구조로 되돌리지 않는다.
- 장치별 동작에 전역 `TargetSerial`을 암묵적으로 사용하지 않는다. DeX,
단일창, 가상 디스플레이, Scrcpy 프로세스, 파일 전송과 정리 작업에는 대상
기기의 세션 또는 serial을 명시적으로 전달한다.
diff --git a/docs/AI_WORKFLOW.md b/docs/AI_WORKFLOW.md
index 0098134..1c83700 100644
--- a/docs/AI_WORKFLOW.md
+++ b/docs/AI_WORKFLOW.md
@@ -2,7 +2,8 @@
## 시작
-1. `docs/README.md`, `PROJECT_BRIEF.md`, `SESSION.md`, `TODO.md`를 읽는다.
+1. `docs/README.md`, `HANDOFF.md`, `PROJECT_BRIEF.md`, `SESSION.md`,
+ `TODO.md`를 읽는다.
2. Git 상태와 최근 커밋을 확인한다.
3. 관련 코드와 기술/결정 문서를 읽는다.
4. DexManager가 실행 중이면 빌드 파일 잠금 여부를 확인한다.
diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md
new file mode 100644
index 0000000..207f148
--- /dev/null
+++ b/docs/HANDOFF.md
@@ -0,0 +1,273 @@
+# DX Manager 새 작업 인수인계
+
+마지막 갱신: 2026-08-22
+
+이 문서는 긴 개발 대화를 보지 못한 새 Codex 작업에서도 DX Manager를 안전하게
+이어가기 위한 현재 기준점이다. 자주 바뀌는 세부 진행 기록은 `SESSION.md`, 남은
+작업은 `TODO.md`, 설계 이유는 `DECISIONS.md`를 함께 사용한다.
+
+## 1. 새 작업에서 가장 먼저 할 일
+
+1. 저장소 루트의 `AGENTS.md`를 읽는다.
+2. 이 문서와 `PROJECT_BRIEF.md`, `SESSION.md`, `TODO.md`를 읽는다.
+3. `git status --short --branch`, `git log -5 --oneline --decorate`와
+ `git describe --tags --always --dirty`를 실행한다.
+4. 미커밋 파일은 모두 메이님의 변경으로 간주하고 원인을 확인하기 전에는
+ 되돌리거나 삭제하지 않는다.
+5. 진단·원인 파악 요청이면 수정하지 않고 먼저 조사 결과와 수정 방향을 설명한다.
+6. 코드 변경 전 관련 서비스, 폼 partial, 모델과 문서의 기존 흐름을 함께 읽는다.
+7. 검증과 배포는 `AI_WORKFLOW.md`를 따른다.
+
+## 2. 현재 공식 기준점
+
+- 제품 이름: **DX Manager**
+- 현재 공개 버전: **v2.0.0**
+- Windows 앱: Assembly/File/Informational version `2.0.0`
+- Android 앱 공개 이름: **DX Companion 2.0.0**, versionCode `6`
+- 공개 태그: `v2.0.0` (`cec74a9`)
+- 공개 태그 뒤 README 대표 이미지 갱신: `e8e47e6`
+- 원격 저장소: `https://github.com/maze-mei/DX-Manager`
+- 이 문서 작성 당시 작업 브랜치: `feature/v2-multi-device`
+- 공개 기준점 이후 제품 코드 변경은 없으며, 다음 기능 후보와 인수인계 문서만
+ 차기 변경으로 관리한다.
+
+정확한 현재 HEAD는 이 문서 자체의 커밋 때문에 위 해시보다 앞설 수 있으므로 항상
+Git 명령으로 다시 확인한다. `main`이라는 로컬 브랜치 이름보다 `origin/main`, 태그와
+실제 HEAD를 기준으로 판단한다.
+
+## 3. 제품과 지원 범위
+
+DX Manager는 Windows에서 Samsung DeX overlay 가상 디스플레이와 Scrcpy 창을
+관리한다. 여러 물리 Galaxy 휴대폰을 동시에 연결해 각 기기별로 DeX 또는 세 개의
+단일 앱 창을 독립 실행하고, 유선·무선 ADB, 입력 보정, 화면 상태, 캡처, 양방향
+파일 전송과 DX Companion을 함께 관리한다.
+
+- Windows: 64비트 Windows 7 SP1, 8.1, 10, 11
+- Windows 런타임: C# WinForms, .NET Framework 4.6.2, x64
+- IDE/빌드 기준: Visual Studio 2019 MSBuild
+- 외부 NuGet 패키지: 없음
+- 번들 Scrcpy: 4.1
+- Android companion: Java 17, min SDK 24, compile/target SDK 36
+- DeX 모드 확인 기준: Android 16 / One UI 8.x의 New DeX 지원 Galaxy
+- One UI 7.x 이하 및 Classic DeX 계열: DeX 모드는 검은 화면 등 정상 동작을
+ 보장하지 않으며 단일창 모드를 사용한다.
+- 32비트 Windows, macOS와 Linux 빌드는 현재 제공하지 않는다.
+
+## 4. 저장소와 산출물 위치
+
+| 위치 | 역할 |
+| --- | --- |
+| `DexManager.sln` | Windows 솔루션 |
+| `DexManager` | WinForms 본체 |
+| `DexManager.AdbProxy` | Unicode 파일 드롭을 중계하는 공개 C# ADB proxy |
+| `DexManager.MultiDeviceTests` | 외부 테스트 프레임워크 없는 다중 기기 회귀 테스트 |
+| `DXDisplayCleanup` | DX Companion Android 소스 |
+| `scripts` | Windows/Android 빌드와 패키징 |
+| `docs` | 개발·사용자·릴리스 문서 |
+| `docs/images` | 공개 README와 설명서 이미지 |
+
+메이님의 원본 작업 및 **정식 릴리스 생성 위치는 항상**
+`E:\vs\dex system`과 `E:\vs\dex system\dist`이다. C 드라이브의 Codex 작업
+사본에 있는 `bin`, `obj`, `dist`는 개발·검증용이며 최종 릴리스라고 안내하지 않는다.
+
+외부 작업 폴더에는 분석용 `repo-code.txt`, 이전 패키지, 다운로드 파일과 별도
+`winget-pkgs` 사본이 섞여 있을 수 있다. 그것들은 이 제품 저장소의 변경으로
+간주하지 않는다. WinGet manifest는 별도 저장소에서 관리한다.
+
+## 5. 핵심 아키텍처
+
+### 물리 기기와 transport
+
+- `PhysicalDeviceRegistry`가 물리 identity를 기준으로 USB와 Wi-Fi ADB serial을
+ 하나의 휴대폰으로 합친다.
+- 표시 이름은 UI 전용이다. 명령 대상은 작업 시작 시 캡처한 명시적 ADB serial이다.
+- `DeviceRuntimeSessionRegistry`와 `DeviceRuntimeServiceSet`은 물리 기기별 독립
+ 서비스 묶음을 소유한다.
+- 기기 탭을 바꿔도 비선택 기기의 DeX·단일창·전송·미니바는 계속 살아 있어야 한다.
+- 연결 정책은 기기별 USB 전용 또는 Wi-Fi 전용이다. 선택 transport가 사라졌다고
+ 반대 transport로 자동 fallback하지 않는다.
+- 같은 휴대폰의 transport가 바뀌면 물리 런타임과 설정은 유지하되 이전 serial에
+ 묶인 reverse, 전송과 세션 자원은 정리한다.
+
+### DeX와 단일창
+
+- `VirtualDisplayService`와 `DexOrchestrator`가 overlay 생성·재사용·정리 및
+ display ID 판정을 담당한다.
+- DeX overlay는 너비, 높이와 DPI가 모두 일치할 때만 재사용한다.
+- display ID는 설정 및 실제 디스플레이 목록의 전후 차이로 찾는다. 가장 큰 숫자
+ 같은 추측값으로 선택하지 않는다.
+- `SingleWindowService`는 Scrcpy `--new-display` 기반 단일창 세 개를 관리한다.
+- Scrcpy 창 제목에는 휴대폰 표시 이름을 포함한다.
+- `ScrcpyLaunchCoordinator`의 시작 직렬화와 프로세스 종료 규칙을 우회하지 않는다.
+
+### 설정과 UI
+
+- `AppSettings`는 공통 설정과 identity별 기기 설정, 단일창 앱 프로필을 저장한다.
+- 기기별 해상도, DPI, 비트레이트, FPS, 앱, USB/Wi-Fi 정책과 자동 재연결 값은
+ 다른 휴대폰과 공유되면 안 된다.
+- `MainForm.*`와 `SettingsForm.*`은 기능별 partial로 나뉘어 있다. 새 기능은
+ 단순히 `MainForm.cs`나 `SettingsForm.cs`에 몰아넣지 않는다.
+- 설정 저장은 고유 임시 파일과 프로세스 간 잠금으로 직렬화된 atomic save다.
+ 다시 공용 `settings.json.tmp` 하나를 사용하면 동시 저장 경합이 재발한다.
+
+### 파일 전송
+
+- PC→휴대폰: Scrcpy 드롭을 `DXMAdbProxy.exe`가 관리형 전송으로 중계한다.
+- proxy는 파일·폴더 push만 가로채며 DX Manager의 일반 ADB 명령에는 관여하지
+ 않는다.
+- Unicode 이름, 폴더 구조, 빈 폴더와 이름 충돌을 보존·처리한다.
+- ADB가 신뢰할 수 있는 byte 진행률을 주지 않으므로 거짓 퍼센트와 남은 시간을
+ 표시하지 않는다.
+- 휴대폰→PC: DX Companion의 공유 메뉴/폴더 선택, 인증 토큰과 기기별 ADB reverse
+ 경로를 사용한다.
+- PC 수신 폴더 아래에 휴대폰 표시 이름별 하위 폴더를 만든다.
+- 한 기기의 연결 해제·취소·토큰 폐기가 다른 기기의 전송에 영향을 주면 안 된다.
+
+### DX Companion과 Windows 종료
+
+- Companion APK는 설치 전 SHA-256과 공식 서명, 설치 후 package·version·서명·
+ 권한을 검증한다. 검증되지 않은 APK에 `WRITE_SECURE_SETTINGS`를 부여하지 않는다.
+- 공개 v2.0.0 APK SHA-256:
+ `7CD40017789E22440DCA0291AB0C45ADB564A19D8A623E669F373395536B880F`
+- Companion은 overlay 제거, DX Manager가 변경한 Stay awake 복원, 빠른 설정 타일,
+ 2 x 1 위젯, 휴대폰→PC 전송만 제공한다. 임의 shell 실행 기능을 추가하지 않는다.
+- 연결 손실 자동 정리 선택지는 즉시, 1분, 5분(기본), 10분, 30분, 정리 안 함이다.
+ 같은 인증 세션이 다시 연결되면 예약 정리를 취소한다.
+- Windows 종료 시 `WM_QUERYENDSESSION` 이후 **새 ADB 프로세스를 실행하지 않는다.**
+ 미리 인증된 Companion loopback 연결로만 PC 종료 메시지를 보낸다.
+- Companion 미설치·미검증 기기는 Windows 종료 중 정리를 건너뛴다. 이때 무리하게
+ ADB를 새로 띄우면 특히 Windows 7에서 네이티브 오류창이 반복될 수 있다.
+- Alt+F8/트레이 종료는 Windows 종료 경로와 다르며 정상 ADB cleanup과 소유
+ 프로세스 정리를 수행한다.
+
+### 입력
+
+- Hangul 키는 scan code만으로 판정하지 않고 `VK_HANGUL` 환경도 고려한다.
+- AltGr/브라질 키보드의 오른쪽 Alt와 한영키를 혼동하지 않는다.
+- Scrcpy 4.x/SDL3 오른쪽 Shift 호환 보정은 실제 회귀 검증 없이 제거하지 않는다.
+- injected 입력과 사용자 직접 Shift+Space를 구분하고 KeyUp 누락 시에도 상태가
+ 고정되지 않게 한다.
+
+## 6. 현재 검증된 상태
+
+- Windows 11에서 Galaxy S26 Ultra와 Galaxy S20 FE를 이용해 USB·Wi-Fi 혼합,
+ 두 무선 기기, 한 기기 DeX와 다른 기기 단일창 동시 실행을 확인했다.
+- 탭별 실행 상태·설정 복원, 연결 해제 격리, transport 정책, 자동 DeX 시작,
+ 양방향 파일 전송과 Companion 관리가 기기별로 분리되는 것을 확인했다.
+- 한 대만 연결하면 기기 선택 UI가 숨고, 두 번째 기기가 연결되면 나타난다.
+- 동시 시작 시 최신 모델 우선, 순차 연결 시 첫 연결 순서를 유지한다.
+- Windows 7 SP1에서 USB 복수 기기와 핵심 기능을 확인했다.
+- .NET Framework 4.6.2 x64 Release 빌드는 경고·오류 없이 통과했다.
+- `DexManager.MultiDeviceTests` 회귀 테스트 39개가 통과했다.
+- DX Companion Android 단위 테스트 7개, `lintRelease`, v2 서명과 인증서 검증이
+ 통과했다.
+- v2.0.0 공개 ZIP의 기록된 SHA-256:
+ `D874021B8C3AC0B4DA7C69CBDFB4492DDE197426F954C8EB639796F26A287EBE`
+- 공개 README 대표 이미지는 태그 뒤 `e8e47e6`에서 최종 교체했다.
+
+실기 결과는 당시 환경의 사실이며 모든 Galaxy/One UI 조합에 대한 보장은 아니다.
+
+## 7. 빌드와 검증
+
+현재 작업 사본에서는 솔루션의 실제 절대 경로를 사용한다. .NET 4.6.2 Developer
+Pack이 없으면 원본 저장소의 참조 어셈블리 경로를 지정한다.
+
+```powershell
+& 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe' `
+ '.\DexManager.sln' /t:Build /p:Configuration=Release `
+ '/p:TargetFrameworkRootPath=E:\vs\dex system\.build-tools\net462\build' /m
+
+& '.\DexManager.MultiDeviceTests\bin\Release\DexManager.MultiDeviceTests.exe'
+```
+
+Android 변경 시:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass `
+ -File .\scripts\Build-AndroidCleanup.ps1
+```
+
+개발 패키지 검증:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass `
+ -File .\scripts\Package-Release.ps1
+```
+
+공식 릴리스는 위 명령의 결과만 믿지 말고 `E:\vs\dex system\dist`에서 다시
+생성·검사한다. 패키지에서 PDB, 로그, screenshot, 사용자 `settings.json`, Android
+서명키·비밀번호를 제외하고 Scrcpy, proxy, Companion, 라이선스와 문서 포함 여부를
+확인한다.
+
+## 8. 다음 작업 우선순위
+
+### 먼저 확인할 항목
+
+1. Windows 7 회사 PC의 실제 Windows 종료에서 ADB 오류창이 사라지고 Companion이
+ overlay와 Stay awake를 복원하는지 재확인한다.
+2. 한국어 노트북 `VK_HANGUL + extended scan 0x38`과 브라질 ABNT/ABNT2·AltGr의
+ `?`, `@`, 악센트 문자 회귀를 실제 키보드에서 확인한다.
+3. Scrcpy 4.1에서도 오른쪽 Shift 보정이 필요한지 확인하고 필요하면 upstream에
+ 재현 정보를 보고한다.
+
+### 합의된 차기 기능 후보
+
+- Android 11+ 보안 무선 ADB 연결 포트가 바뀌는 문제를 줄이기 위한 mDNS endpoint
+ 자동 발견. 상세 안전 조건과 테스트 행렬은 `TODO.md`에 작성되어 있다.
+- WinGet의 v2.0.0 manifest 상태 확인 및 최신 공개 자산 해시 반영.
+
+### 테스트를 먼저 추가한 뒤 가능한 구조 개선
+
+- Scrcpy 주 창/단일창의 종료 및 stdout/stderr drain 공통화
+- `PhoneTransferReceiver`의 프로토콜과 저장 경로/충돌 처리 분리
+- `WirelessAdbService`의 기기 명령과 자동 재연결 snapshot 계산 분리
+- `AppSettings.EnsureDefaults`의 설정 범주별 helper 분리
+
+### 논의됐지만 아직 확정하지 않은 장기 후보
+
+- 기존 기능을 유지한 전체 UI/UX 재설계. 디자인 시스템을 먼저 확정하고 WinForms,
+ Windows 7, 한·영문 길이와 DPI를 기준으로 단계적으로 적용한다.
+- v2 안정화 뒤 Windows 10/11 전용 MSIX/Microsoft Store 변형. 기존 포터블
+ .NET 4.6.2/Windows 7 빌드는 별도로 유지한다.
+- One UI 9 정식판이 나온 뒤 실제 기기 호환성 확인.
+- macOS/Linux 포팅은 테스트 장비와 별도 UI·플랫폼 구현이 필요해 현재 계획이 없다.
+
+## 9. 회귀를 막기 위한 금지 사항
+
+- 시스템 PATH의 `adb.exe`를 사용하지 않는다.
+- 장치 명령에서 암묵적 전역 target serial 또는 `ANDROID_SERIAL`에 의존하지 않는다.
+- 연결된 첫 serial, 가장 큰 display ID 또는 같은 모델이라는 이유로 대상을
+ 추측하지 않는다.
+- 사용자가 USB 전용으로 정한 기기를 Wi-Fi로, Wi-Fi 전용 기기를 USB로 자동
+ 전환하지 않는다.
+- Windows 종료 중 새 ADB·Scrcpy·보조 프로세스를 시작하지 않는다.
+- 한 기기 cleanup 때문에 모든 ADB나 다른 경로의 동명 프로세스를 종료하지 않는다.
+- Companion 서명 검증을 생략하거나 APK를 사용자 동의 없이 자동 설치하지 않는다.
+- 관리형 전송 UI에 검증되지 않은 퍼센트·남은 시간을 표시하지 않는다.
+- 사용자 설정, 로그, 캡처, 서명 비밀, 외부 분석 파일을 Git에 넣지 않는다.
+- 공개용 언어 순서는 영어, 한국어 순서를 유지한다.
+
+## 10. 메이님과 작업할 때의 진행 방식
+
+- 사용자를 **메이님**이라고 부른다.
+- 메이님은 문제를 먼저 설명하고 원인·대책을 합의한 뒤 “수정하자”고 요청하는
+ 흐름을 선호한다. 원인 파악 단계에서 선제 수정하지 않는다.
+- 수정 요청을 받으면 합의된 범위는 자율적으로 구현·검증하되, 공개 push, tag,
+ GitHub Release와 외부 게시물 수정은 별도 승인을 받는다.
+- 실기 확인은 메이님이 Windows 11/Windows 7과 실제 Galaxy 기기에서 수행한다.
+ 결과를 코드 테스트 성공과 혼동하지 않는다.
+- 스크린샷과 로그 폴더의 테스트 파일은 배포에 넣지 않는다. 삭제 전 경로가 저장소
+ 내부 생성물인지 확인한다.
+- 정식 릴리스는 `E:\vs\dex system\dist`에서 만든다.
+
+## 11. 새 작업 시작용 문구
+
+다음 문구를 새 Codex 작업 첫 메시지로 사용할 수 있다.
+
+> DX Manager 작업을 이어가자. 저장소의 AGENTS.md와
+> docs/HANDOFF.md, PROJECT_BRIEF.md, SESSION.md, TODO.md,
+> AI_WORKFLOW.md를 먼저 전부 읽고 Git 상태와 최근 커밋을 확인해줘.
+> 현재 공개 기준은 v2.0.0이며 여러 휴대폰을 물리 identity별 독립 세션으로
+> 관리한다. 기존 동작과 미커밋 변경을 보존하고, 바로 수정하지 말라는 요청이면
+> 원인과 수정 방향만 먼저 설명해줘. 정식 릴리스 위치는
+> E:\vs\dex system\dist야.
diff --git a/docs/PROJECT_BRIEF.md b/docs/PROJECT_BRIEF.md
index c2497ad..3a49a4a 100644
--- a/docs/PROJECT_BRIEF.md
+++ b/docs/PROJECT_BRIEF.md
@@ -83,10 +83,10 @@ GitHub 메인 README와 별도로 HTML이 없는 `docs\PACKAGE_README.md`를 배
## 현재 상태
-v1.3.0까지 공개 배포됐고 v2.0.0 공개 후보를 준비했다. Windows 11에서
+v2.0.0과 DX Companion 2.0.0을 공개 배포했다. Windows 11에서
휴대폰 두 대의 USB·무선 조합, 독립 DeX·단일창, 설정, Companion과 양방향
전송을 확인했다. 64비트 Windows 7 SP1/.NET 4.6.2에서는 USB 복수 기기와
-핵심 기능을 확인했다. v2.0.0은 물리 기기별 런타임·설정·연결 정책과
+핵심 기능을 확인했다. 공개 v2.0.0은 물리 기기별 런타임·설정·연결 정책과
Companion 2.0.0 종료 보호를 포함한다. 설정의 진단 페이지에는 현재 선택된
휴대폰의 Android·SDK·One UI·보안 패치와 연결 방식을 표시하고, 민감 정보를
가린 진단 보고서를 텍스트 파일로 저장하는 기능도 포함한다.
@@ -97,9 +97,10 @@ Companion 2.0.0 종료 보호를 포함한다. 설정의 진단 페이지에는
앱 아이콘, 다국어 UI, 개발 문서와 제3자 라이선스 고지는 완료됐다.
한국어/영어 FAQ도 독립 사용자 문서로 완료됐으며 README, 사용 설명서와
FAQ의 스크린샷 배치도 완료됐다. 공개용 x64 ZIP을 만들고 개인 설정, PDB,
-로그와 테스트 스크린샷이 포함되지 않은 것도 확인했다. GitHub 저장소를
-public으로 전환하고 `DX Manager v1.0.0` Release와 x64 ZIP 게시도 완료했다.
-최신 빌드의 64비트 Windows 7 회귀 확인과 upstream 입력 버그 보고가 남아 있다.
+로그와 테스트 스크린샷이 포함되지 않은 것도 확인했다. GitHub 저장소와
+`DX Manager v2.0.0` Release, x64 ZIP 및 최종 대표 이미지 게시도 완료했다.
+Windows 7 실제 종료 복원 재확인, 일부 키보드 실기 확인, 무선 ADB endpoint
+자동 발견과 upstream 입력 버그 보고가 차기 작업으로 남아 있다.
## 제품 원칙
diff --git a/docs/README.md b/docs/README.md
index 5dcd8c5..f9cc191 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,13 +1,14 @@
# DexManager 개발 문서
-마지막 전체 정리: 2026-08-18
+마지막 전체 정리: 2026-08-22
-새 채팅에서는 `PROJECT_BRIEF.md`, `SESSION.md`, `TODO.md`,
+새 작업에서는 `HANDOFF.md`, `PROJECT_BRIEF.md`, `SESSION.md`, `TODO.md`,
`AI_WORKFLOW.md` 순서로 읽는다. 작업 성격에 따라 `TECH_NOTES.md`,
`DECISIONS.md`, `KNOWN_ISSUES.md`를 추가로 확인한다.
문서와 코드가 다르면 현재 코드와 Git 이력을 우선하고 문서를 갱신한다.
+- `HANDOFF.md`: 새 작업용 현재 기준점, 아키텍처, 불변 조건과 검증 절차
- `PROJECT_BRIEF.md`: 목적, 환경, 핵심 기능
- `TECH_NOTES.md`: 구현 구조와 기술 주의사항
- `DECISIONS.md`: 현재 구조를 선택한 이유
diff --git a/docs/SESSION.md b/docs/SESSION.md
index 723eaab..565f9f2 100644
--- a/docs/SESSION.md
+++ b/docs/SESSION.md
@@ -1,14 +1,36 @@
# Session Handoff
-마지막 갱신: 2026-08-18
+마지막 갱신: 2026-08-22
## Git
- 저장소 작업 사본: 현재 Codex 작업 폴더의 `v12-docs`
- 원본 저장소: `E:\vs\dex system`
- 브랜치: `feature/v2-multi-device`
-- 마지막 공개 커밋: 이 문서를 포함한 최신 `main` (`git log -1`로 확인)
-- 현재 작업: v2 다중 기기 구현과 실기 확인 완료, v2.0.0 공개 후보 패키징
+- 공개 태그: `v2.0.0` (`cec74a9`)
+- 공개 태그 뒤 대표 이미지 갱신: `e8e47e6`
+- 현재 작업: v2.0.0 공개 완료, 새 작업 이관 문서와 차기 후보 정리
+
+새 작업에서는 먼저 `docs/HANDOFF.md`를 읽는다. 공개 v2.0.0 이후 제품 코드
+변경은 없으며, 차기 작업은 `TODO.md`의 미완료 항목으로 관리한다. 현재 브랜치와
+HEAD는 이 문서가 다시 바뀔 수 있으므로 `git status`와 `git log`로 확인한다.
+
+## 2026-08-22 v2.0.0 공개 및 작업 이관
+
+DX Manager v2.0.0과 DX Companion 2.0.0을 GitHub에 공개했고, 영어·한국어
+README·사용 설명서·FAQ·릴리스 설명 및 다중 기기/진단/Companion 스크린샷을
+현재 기능에 맞게 갱신했다. 공개 태그 뒤 대표 이미지에 실제 DeX 아이콘이 보이는
+최종 스크린샷을 반영한 커밋이 `e8e47e6`이다.
+
+다음 작업이 긴 대화 기록 없이도 안전하게 이어지도록 현재 제품 기준점, 저장소
+구조, 다중 기기·전송·Companion·Windows 종료 불변 조건, 검증 명령, 실기 확인
+범위와 차기 후보를 `docs/HANDOFF.md`에 통합했다. `AGENTS.md`와 개발 문서의
+“v2 준비 중” 표현도 공개 완료 상태로 바로잡았다.
+
+이관 시점의 제품 소스는 공개 v2.0.0과 동일하다. 새로 합의된 기능 후보는 Android
+11+ 무선 ADB 연결 endpoint의 mDNS 자동 발견이며, 페어링 포트와 연결 포트를
+분리하고 동일 모델 복수 기기를 추측으로 선택하지 않는 상세 조건을 `TODO.md`에
+기록했다. 구현은 아직 시작하지 않았다.
Windows 종료 중 `adb.exe` 네이티브 오류창이 반복되는 문제 때문에 실제 종료
경로를 일반적인 Alt+F8·트레이 종료와 완전히 분리했다. `WM_QUERYENDSESSION`을
diff --git a/docs/TODO.md b/docs/TODO.md
index bac6b95..25a3913 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -22,6 +22,30 @@
- 대용량 전송·중단·재연결 테스트를 먼저 보강한 뒤 진행한다.
- [ ] `WirelessAdbService`의 기기별 연결 명령과 자동 재연결 snapshot 계산을 분리
- USB·무선 동시 연결과 여러 무선 기기의 격리 테스트를 유지한다.
+- [ ] Android 11+ 무선 ADB의 현재 연결 endpoint를 mDNS로 자동 발견
+ - 번들 ADB의 `adb mdns services`에서 `_adb-tls-connect._tcp` 서비스를 조회하고,
+ 페어링용 `_adb-tls-pairing._tcp` 포트와 실제 연결 포트를 명확히 구분한다.
+ - 검색 결과의 첫 항목을 임의로 선택하지 않는다. 물리 기기 identity, 기존 USB
+ serial, 모델과 이미 검증된 transport를 대조해 현재 선택한 휴대폰의 endpoint만
+ 갱신하며, 같은 모델 여러 대와 복수 무선 기기를 안전하게 구분한다.
+ - 휴대폰 재부팅, 무선 디버깅 재활성화, Wi-Fi 변경 등으로 연결 포트가 바뀌면
+ 자동 재연결 전에 새 endpoint를 다시 발견한다. 검증에 성공한 경우에만 기기별
+ `WirelessHost`와 `WirelessPort`를 갱신한다.
+ - mDNS를 사용할 수 없거나 방화벽·공유기가 multicast 검색을 차단하면 현재의
+ 수동 IP·연결 포트 입력과 저장된 endpoint를 폴백으로 유지한다.
+ - 사용자가 선택한 USB 전용·무선 전용 연결 정책은 그대로 지킨다. 자동 발견을
+ 이유로 USB와 Wi-Fi 사이를 임의로 전환하지 않는다.
+ - Windows 7의 legacy ADB와 `USB로 무선 준비`의 고정 5555 방식은 지원 여부와
+ 동작이 다르므로 Android 11+ 보안 무선 디버깅 경로와 분리한다.
+ - 로그에는 발견 서비스, 대상 기기 판정 근거, 이전/새 endpoint, 폴백 사유와
+ 연결 결과를 남기되 6자리 페어링 코드와 인증 정보는 기록하지 않는다.
+ - 한 대, 같은 모델 두 대, USB+Wi-Fi 혼합, 무선 두 대, 포트 변경, stale 서비스,
+ mDNS 차단과 수동 폴백을 독립 회귀 테스트와 실제 기기에서 확인한다.
+ - 구현과 실기 검증이 끝난 뒤 README, 영어·한국어 사용 설명서와 FAQ의 무선 ADB
+ 안내를 갱신한다. 기본 흐름은 `최초 1회 페어링 -> 연결 endpoint 자동 발견`으로
+ 설명하고, 수동 IP·포트 입력은 폴백으로 안내한다. 페어링 포트와 연결 포트가
+ 서로 다르며 연결 포트는 바뀔 수 있다는 설명, legacy 5555 방식, 관련 스크린샷과
+ 캡션도 함께 수정한다.
- [ ] `AppSettings.EnsureDefaults`를 설정 범주별 정규화 helper로 축소
- 기존 스키마 설정 파일 마이그레이션 표본을 만든 뒤 진행한다.
- [x] v2 1단계 물리 기기·ADB transport 기반
@@ -157,8 +181,9 @@
- [x] v1.3.0 공개 Release 게시와 DX Companion 번들 배포
- [x] DX Manager 자체 소스 라이선스를 MIT로 확정
-별도 설치 프로그램은 보류한다. v1.3.0 WinGet manifest는 등록됐으며,
-v2.0.0 공개 Release가 게시된 뒤 해당 자산의 해시로 manifest를 갱신한다.
+별도 설치 프로그램은 보류한다. v1.3.0 WinGet manifest는 등록됐다.
+- [ ] 별도 `winget-pkgs` 작업 사본에서 v2.0.0 manifest의 제출·승인 상태를
+ 확인하고, 필요하면 공개 자산 URL과 SHA-256으로 갱신한다.
## 독립 FAQ 반영 항목
From be181f565590e6fb5604f36bcf9a09d499415f4f Mon Sep 17 00:00:00 2001
From: maze-mei
Date: Sun, 23 Aug 2026 13:06:49 +0900
Subject: [PATCH 2/6] Release DX Manager 2.0.1
---
.../Forms/MainForm.DeviceConnections.cs | 34 +++++++-
DexManager/Forms/MainForm.Devices.cs | 9 ++-
DexManager/Properties/AssemblyInfo.cs | 6 +-
DexManager/README.md | 2 +-
DexManager/licenses/THIRD_PARTY_NOTICES.md | 2 +-
README.md | 10 +--
docs/AI_WORKFLOW.md | 2 +-
docs/CHANGELOG.md | 8 ++
docs/HANDOFF.md | 7 +-
docs/PACKAGE_README.md | 4 +-
docs/PROJECT_BRIEF.md | 4 +
docs/README.md | 3 +-
docs/RELEASE_NOTES_v2.0.1.md | 78 +++++++++++++++++++
docs/SESSION.md | 28 +++++--
docs/TODO.md | 6 ++
scripts/Package-Release.ps1 | 2 +-
16 files changed, 180 insertions(+), 25 deletions(-)
create mode 100644 docs/RELEASE_NOTES_v2.0.1.md
diff --git a/DexManager/Forms/MainForm.DeviceConnections.cs b/DexManager/Forms/MainForm.DeviceConnections.cs
index 002e373..2d74b34 100644
--- a/DexManager/Forms/MainForm.DeviceConnections.cs
+++ b/DexManager/Forms/MainForm.DeviceConnections.cs
@@ -70,7 +70,23 @@ private void ReconcileDeviceTabs(DeviceRegistrySnapshot snapshot)
context.Device != null &&
context.Device.IsConnected)
{
- SelectDeviceContext(context);
+ if (initialSelectionPending)
+ {
+ if (ReferenceEquals(
+ context,
+ _selectedDeviceContext))
+ {
+ RefreshInitiallyBoundDeviceSettings(context);
+ }
+ else
+ {
+ SelectDeviceContext(context, false);
+ }
+ }
+ else
+ {
+ SelectDeviceContext(context);
+ }
break;
}
}
@@ -82,6 +98,22 @@ private void ReconcileDeviceTabs(DeviceRegistrySnapshot snapshot)
_settingsForm.RefreshSelectedDeviceContext();
}
+ private void RefreshInitiallyBoundDeviceSettings(
+ DeviceUiContext context)
+ {
+ if (context == null) return;
+
+ // The main form initially displays the legacy default template
+ // before a physical device identity is known. The first device
+ // reuses that already-selected context, so SelectDeviceContext()
+ // intentionally returns without reloading the controls. Reload
+ // directly here and do not save the stale template values into
+ // the newly bound per-device profile.
+ _selectedDeviceIdentity = context.Identity ?? string.Empty;
+ _modeSettingsDirty = context.ModeSettingsDirty ?? new bool[4];
+ LoadRunSettings();
+ }
+
private DeviceUiContext EnsureDeviceContext(PhysicalDeviceInfo device)
{
DeviceUiContext context;
diff --git a/DexManager/Forms/MainForm.Devices.cs b/DexManager/Forms/MainForm.Devices.cs
index 5ab6bc1..1cc9aa2 100644
--- a/DexManager/Forms/MainForm.Devices.cs
+++ b/DexManager/Forms/MainForm.Devices.cs
@@ -169,6 +169,13 @@ private static int CountConnectedPhysicalDevices(
}
private void SelectDeviceContext(DeviceUiContext context)
+ {
+ SelectDeviceContext(context, true);
+ }
+
+ private void SelectDeviceContext(
+ DeviceUiContext context,
+ bool saveCurrentMode)
{
if (context == null || ReferenceEquals(
context,
@@ -178,7 +185,7 @@ private void SelectDeviceContext(DeviceUiContext context)
return;
}
- SaveCurrentModeBeforeSwitch();
+ if (saveCurrentMode) SaveCurrentModeBeforeSwitch();
if (_selectedDeviceContext != null)
{
_selectedDeviceContext.SelectedMode = _selectedMode;
diff --git a/DexManager/Properties/AssemblyInfo.cs b/DexManager/Properties/AssemblyInfo.cs
index 1060110..04e93e5 100644
--- a/DexManager/Properties/AssemblyInfo.cs
+++ b/DexManager/Properties/AssemblyInfo.cs
@@ -11,6 +11,6 @@
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("c5cc4a3f-7ad3-449b-b89c-49c4f4604d19")]
-[assembly: AssemblyVersion("2.0.0.0")]
-[assembly: AssemblyFileVersion("2.0.0.0")]
-[assembly: AssemblyInformationalVersion("2.0.0")]
+[assembly: AssemblyVersion("2.0.1.0")]
+[assembly: AssemblyFileVersion("2.0.1.0")]
+[assembly: AssemblyInformationalVersion("2.0.1")]
diff --git a/DexManager/README.md b/DexManager/README.md
index 48b046e..9bfd984 100644
--- a/DexManager/README.md
+++ b/DexManager/README.md
@@ -31,7 +31,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\Package-Release.ps
```
The script rebuilds Release and writes `dist/DX Manager` and
-`dist/DX-Manager-v2.0.0-win-x64.zip`. Use `-SkipBuild` only when the current
+`dist/DX-Manager-v2.0.1-win-x64.zip`. Use `-SkipBuild` only when the current
Release output has already been verified. `ExecutionPolicy Bypass` applies
only to this process and does not change the system policy.
diff --git a/DexManager/licenses/THIRD_PARTY_NOTICES.md b/DexManager/licenses/THIRD_PARTY_NOTICES.md
index 750d0e7..261572e 100644
--- a/DexManager/licenses/THIRD_PARTY_NOTICES.md
+++ b/DexManager/licenses/THIRD_PARTY_NOTICES.md
@@ -1,4 +1,4 @@
-# DX Manager v2.0.0 - Third-Party Notices
+# DX Manager v2.0.1 - Third-Party Notices
DX Manager is an independently developed utility. The third-party binaries
listed below are distributed without modification and remain under their
diff --git a/README.md b/README.md
index d7230c1..2932f58 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
한국어 사용 설명서 ·
FAQ ·
Q&A ·
- v2.0.0 release notes ·
+ v2.0.1 release notes ·
Third-party notices
@@ -299,12 +299,12 @@ Open `DexManager.sln` and build the `Release` configuration. The output is
written to `DexManager/bin/Release`. To create the public portable folder and
ZIP, run `scripts/Package-Release.ps1`. It keeps the developer output in place
and writes `dist/DX Manager` plus
-`dist/DX-Manager-v2.0.0-win-x64.zip`. See
+`dist/DX-Manager-v2.0.1-win-x64.zip`. See
[DexManager/README.md](DexManager/README.md) for packaging notes.
## Project Status
-Version 2.0.0 bundles scrcpy 4.1. The current verification baseline includes:
+Version 2.0.1 bundles scrcpy 4.1. The current verification baseline includes:
- Windows 11: two-phone USB/Wi-Fi combinations with independent DeX,
single-app windows, settings, Companion sessions, and bidirectional transfers
@@ -623,12 +623,12 @@ DX Manager는 SDL3 기반 scrcpy 4.x 창과의 호환을 위해 물리 오른쪽
`DexManager.sln`을 열고 `Release` 구성으로 빌드합니다. 결과물은
`DexManager/bin/Release`에 생성됩니다. 공개용 포터블 폴더와 ZIP은
`scripts/Package-Release.ps1`을 실행해 만듭니다. 개발 빌드 폴더는 유지하고
-`dist/DX Manager`와 `dist/DX-Manager-v2.0.0-win-x64.zip`을 생성합니다. 배포 파일 구성은
+`dist/DX Manager`와 `dist/DX-Manager-v2.0.1-win-x64.zip`을 생성합니다. 배포 파일 구성은
[DexManager/README.md](DexManager/README.md)를 참조하십시오.
## 프로젝트 상태
-버전 2.0.0은 scrcpy 4.1을 포함합니다. 현재 확인 기준은 다음과 같습니다.
+버전 2.0.1은 scrcpy 4.1을 포함합니다. 현재 확인 기준은 다음과 같습니다.
- Windows 11: 휴대폰 두 대의 USB·Wi-Fi 조합, 독립 DeX·단일창·설정·
Companion 세션과 양방향 전송
diff --git a/docs/AI_WORKFLOW.md b/docs/AI_WORKFLOW.md
index 1c83700..e52858f 100644
--- a/docs/AI_WORKFLOW.md
+++ b/docs/AI_WORKFLOW.md
@@ -61,7 +61,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\Package-Release.ps
안내하지 않는다.
스크립트는 DX Manager 실행 여부를 확인하고 번들 Release ADB 서버를 정리한
뒤 Debug/Release의 로그와 스크린샷 테스트 파일을 비운다.
-v2.0.0 패키지는 Scrcpy 4.1 런타임,
+v2.0.1 패키지는 Scrcpy 4.1 런타임,
`tools\adb-proxy\DXMAdbProxy.exe`와 서명이 검증된
`tools\companion\DX-Companion.apk`를 반드시 포함해야 한다.
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 076f0f1..769f40c 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -2,6 +2,14 @@
세부 변경은 Git 이력을 사용하고 여기에는 큰 이정표만 적는다.
+## 2026-08 - v2.0.1
+
+- 프로그램 시작 시 첫 물리 휴대폰 identity가 초기 UI 컨텍스트에 결속된 뒤
+ 기기별 DeX 해상도·DPI·비트레이트·FPS·실행 옵션을 다시 표시하도록 수정
+- 시작부터 여러 휴대폰이 연결된 경우에도 identity 확정 전 표시한 공통 기본값을
+ 어느 휴대폰의 DeX 설정에도 저장하지 않도록 최초 선택 경로를 분리
+- DX Companion은 변경 없이 검증된 2.0.0(versionCode 6) APK를 유지
+
## 2026-08 - v2.0.0
- 구조 감사에서 더 이상 구독되지 않던 v1 단일 기기 연결·분리 이벤트 처리기를
diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md
index 207f148..98681b0 100644
--- a/docs/HANDOFF.md
+++ b/docs/HANDOFF.md
@@ -22,14 +22,15 @@
- 제품 이름: **DX Manager**
- 현재 공개 버전: **v2.0.0**
-- Windows 앱: Assembly/File/Informational version `2.0.0`
+- GitHub 업로드 준비 버전: **v2.0.1**
+- Windows 앱: Assembly/File/Informational version `2.0.1`
- Android 앱 공개 이름: **DX Companion 2.0.0**, versionCode `6`
- 공개 태그: `v2.0.0` (`cec74a9`)
- 공개 태그 뒤 README 대표 이미지 갱신: `e8e47e6`
- 원격 저장소: `https://github.com/maze-mei/DX-Manager`
- 이 문서 작성 당시 작업 브랜치: `feature/v2-multi-device`
-- 공개 기준점 이후 제품 코드 변경은 없으며, 다음 기능 후보와 인수인계 문서만
- 차기 변경으로 관리한다.
+- v2.0.1은 최초 물리 기기 결속 뒤 DeX 기기별 설정이 공통 기본값으로 표시되던
+ UI 동기화 회귀만 수정한 유지보수 릴리스다.
정확한 현재 HEAD는 이 문서 자체의 커밋 때문에 위 해시보다 앞설 수 있으므로 항상
Git 명령으로 다시 확인한다. `main`이라는 로컬 브랜치 이름보다 `origin/main`, 태그와
diff --git a/docs/PACKAGE_README.md b/docs/PACKAGE_README.md
index 8e55152..bcd6ca0 100644
--- a/docs/PACKAGE_README.md
+++ b/docs/PACKAGE_README.md
@@ -1,7 +1,7 @@
# DX Manager
Portable Windows package for managing Samsung DeX and up to three independent
-scrcpy app windows per connected Galaxy phone. Version 2.0.0 supports multiple
+scrcpy app windows per connected Galaxy phone. Version 2.0.1 supports multiple
physical phones simultaneously and bundles scrcpy 4.1.
## English
@@ -164,7 +164,7 @@ Project page: https://github.com/maze-mei/DX-Manager
여러 Galaxy 휴대폰의 Samsung DeX 가상 디스플레이와 휴대폰별 scrcpy
단일창을 최대 3개까지 동시에 관리하는 Windows용 포터블 프로그램입니다.
-버전 2.0.0은 scrcpy 4.1을 포함합니다.
+버전 2.0.1은 scrcpy 4.1을 포함합니다.
### 요구 사항
diff --git a/docs/PROJECT_BRIEF.md b/docs/PROJECT_BRIEF.md
index 3a49a4a..b8b40fa 100644
--- a/docs/PROJECT_BRIEF.md
+++ b/docs/PROJECT_BRIEF.md
@@ -83,6 +83,10 @@ GitHub 메인 README와 별도로 HTML이 없는 `docs\PACKAGE_README.md`를 배
## 현재 상태
+v2.0.1은 첫 물리 휴대폰 identity가 확정된 뒤에도 DeX 화면에 공통 기본값이
+남아 있던 UI 동기화 회귀를 수정한 GitHub 업로드 후보이다. DX Companion은
+변경 없이 2.0.0(versionCode 6)을 유지한다.
+
v2.0.0과 DX Companion 2.0.0을 공개 배포했다. Windows 11에서
휴대폰 두 대의 USB·무선 조합, 독립 DeX·단일창, 설정, Companion과 양방향
전송을 확인했다. 64비트 Windows 7 SP1/.NET 4.6.2에서는 USB 복수 기기와
diff --git a/docs/README.md b/docs/README.md
index f9cc191..59eb892 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -17,7 +17,8 @@
- `SESSION.md`: 다음 채팅을 위한 현재 상태
- `AI_WORKFLOW.md`: Codex 작업 및 Git 규칙
- `CHANGELOG.md`: 사용자 관점의 큰 이정표
-- `RELEASE_NOTES_v2.0.0.md`: v2.0.0 GitHub Release용 영어·한국어 설명
+- `RELEASE_NOTES_v2.0.1.md`: v2.0.1 GitHub Release용 영어·한국어 설명
+- `RELEASE_NOTES_v2.0.0.md`: 이전 v2.0.0 GitHub Release 기록
- `RELEASE_NOTES_v1.3.0.md`: 이전 v1.3.0 GitHub Release 기록
- `PACKAGE_README.md`: 배포 ZIP 루트에 들어가는 HTML 없는 영어·한국어 안내
- `../DXDisplayCleanup`: 번들 선택형 Android 복구·파일 전송 앱 소스와 빌드 문서
diff --git a/docs/RELEASE_NOTES_v2.0.1.md b/docs/RELEASE_NOTES_v2.0.1.md
new file mode 100644
index 0000000..3f0b291
--- /dev/null
+++ b/docs/RELEASE_NOTES_v2.0.1.md
@@ -0,0 +1,78 @@
+# DX Manager v2.0.1
+
+## English
+
+DX Manager v2.0.1 is a maintenance release that fixes a DeX settings display
+regression introduced with per-device settings in v2.0.0.
+
+### Fixed
+
+- Reload the selected phone's DeX resolution, DPI, bitrate, FPS, and launch
+ options after the first physical-device identity is established.
+- Prevent the legacy default values shown before device identification from
+ being saved over a phone's existing DeX profile during initial selection.
+- Preserve the same protection when multiple phones are already connected at
+ startup. Single-Window settings remain independent and unchanged.
+
+### DX Companion compatibility
+
+DX Companion is unchanged in this release. The package continues to bundle the
+verified DX Companion 2.0.0 APK (versionCode 6), and DX Manager still validates
+its package name, version, SHA-256, and official signing certificate before
+installation or privileged use.
+
+### Compatibility
+
+- 64-bit Windows 7 SP1, 8.1, 10, or 11
+- .NET Framework 4.6.2 or later
+- Bundled scrcpy 4.1
+- Samsung Galaxy device with DeX support
+
+Extract the complete portable folder and run `DXManager.exe`. Do not copy the
+executable by itself.
+
+### SHA-256
+
+- `DX-Manager-v2.0.1-win-x64.zip`: `5F9C5A6AF6199D38458F6266869DDBACC58722A3A915696FF02935CF8965B2C1`
+- `DXManager.exe`: `F94C6EDC43DEDF742E0885E63A8E7D1B0104385B36A29F20D1BC55495FF466EB`
+- Bundled `DX-Companion.apk`:
+ `7CD40017789E22440DCA0291AB0C45ADB564A19D8A623E669F373395536B880F`
+
+---
+
+## 한국어
+
+DX Manager v2.0.1은 v2.0.0의 기기별 설정 도입 뒤 발생한 DeX 설정 표시 회귀를
+수정한 유지보수 릴리스입니다.
+
+### 수정 사항
+
+- 첫 물리 휴대폰 identity가 확정된 뒤 선택 기기의 DeX 해상도·DPI·비트레이트·
+ FPS와 실행 옵션을 다시 불러와 정확히 표시합니다.
+- 기기 식별 전에 표시한 공통 기본값이 최초 선택 과정에서 기존 기기별 DeX
+ 프로필을 덮어쓰지 않도록 수정했습니다.
+- 여러 휴대폰이 시작부터 연결된 경우에도 같은 보호를 적용합니다. 단일창의
+ 기기별 설정과 동작은 변경하지 않았습니다.
+
+### DX Companion 호환성
+
+이번 릴리스에서 DX Companion은 변경되지 않았습니다. 검증된 DX Companion
+2.0.0 APK(versionCode 6)를 계속 포함하며, DX Manager는 설치 또는 권한 기능
+사용 전에 package 이름, 버전, SHA-256과 공식 서명 인증서를 계속 검증합니다.
+
+### 호환성
+
+- 64비트 Windows 7 SP1, 8.1, 10 또는 11
+- .NET Framework 4.6.2 이상
+- 번들 scrcpy 4.1
+- Samsung DeX 지원 Galaxy 기기
+
+포터블 폴더 전체의 압축을 푼 뒤 `DXManager.exe`를 실행하십시오. 실행 파일만
+따로 복사하지 마십시오.
+
+### SHA-256
+
+- `DX-Manager-v2.0.1-win-x64.zip`: `5F9C5A6AF6199D38458F6266869DDBACC58722A3A915696FF02935CF8965B2C1`
+- `DXManager.exe`: `F94C6EDC43DEDF742E0885E63A8E7D1B0104385B36A29F20D1BC55495FF466EB`
+- 번들 `DX-Companion.apk`:
+ `7CD40017789E22440DCA0291AB0C45ADB564A19D8A623E669F373395536B880F`
diff --git a/docs/SESSION.md b/docs/SESSION.md
index 565f9f2..fd0853f 100644
--- a/docs/SESSION.md
+++ b/docs/SESSION.md
@@ -1,6 +1,6 @@
# Session Handoff
-마지막 갱신: 2026-08-22
+마지막 갱신: 2026-08-23
## Git
@@ -9,11 +9,29 @@
- 브랜치: `feature/v2-multi-device`
- 공개 태그: `v2.0.0` (`cec74a9`)
- 공개 태그 뒤 대표 이미지 갱신: `e8e47e6`
-- 현재 작업: v2.0.0 공개 완료, 새 작업 이관 문서와 차기 후보 정리
+- 현재 작업: v2.0.1 DeX 설정 표시 회귀 수정과 GitHub 업로드 패키지 준비 완료
-새 작업에서는 먼저 `docs/HANDOFF.md`를 읽는다. 공개 v2.0.0 이후 제품 코드
-변경은 없으며, 차기 작업은 `TODO.md`의 미완료 항목으로 관리한다. 현재 브랜치와
-HEAD는 이 문서가 다시 바뀔 수 있으므로 `git status`와 `git log`로 확인한다.
+새 작업에서는 먼저 `docs/HANDOFF.md`를 읽는다. 공개 v2.0.0 이후 v2.0.1의 DeX
+설정 표시 회귀 수정이 반영됐으며, 차기 작업은 `TODO.md`의 미완료 항목으로
+관리한다. 현재 브랜치와 HEAD는 이 문서가 다시 바뀔 수 있으므로 `git status`와
+`git log`로 확인한다.
+
+## 2026-08-23 v2.0.1 DeX 설정 표시 회귀 수정
+
+프로그램 시작 시 UI가 물리 휴대폰 identity를 알기 전에 공통 기본 설정을 먼저
+표시하고, 첫 휴대폰이 기존 초기 컨텍스트를 재사용하면 기기별 설정을 다시 읽지
+않는 문제를 수정했다. 첫 identity 결속 뒤 선택 기기의 DeX 해상도·DPI·비트레이트·
+FPS와 실행 옵션을 다시 표시한다. 여러 휴대폰이 시작부터 연결된 경우에도 최초
+선택 전 공통 기본값을 기존 기기 프로필에 저장하지 않는다.
+
+Windows 앱 버전은 2.0.1이다. Android 소스와 프로토콜은 변경하지 않았으며 번들
+DX Companion은 기존의 검증된 2.0.0(versionCode 6)을 유지한다.
+
+최종 Release 재빌드는 경고 0, 오류 0이며 다중 기기 회귀 테스트 39개를 모두
+통과했다. GitHub 업로드용 ZIP은 64개 항목이고, PDB·사용자 설정·로그·스크린샷·
+서명 비밀 파일을 포함하지 않는다. 최종 산출물은 `E:\vs\dex system\dist`에
+배치했다. ZIP SHA-256은
+`5F9C5A6AF6199D38458F6266869DDBACC58722A3A915696FF02935CF8965B2C1`이다.
## 2026-08-22 v2.0.0 공개 및 작업 이관
diff --git a/docs/TODO.md b/docs/TODO.md
index 25a3913..fe02a28 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -2,6 +2,12 @@
## 다음 작업
+- [x] v2.0.1 DeX 기기별 설정 표시 회귀 수정
+ - [x] 첫 물리 identity 결속 뒤 기기별 DeX 설정 UI 재동기화
+ - [x] 최초 다중 기기 선택에서 공통 기본값의 기기별 설정 덮어쓰기 차단
+ - [x] Windows 버전 2.0.1과 기존 DX Companion 2.0.0(versionCode 6) 호환 확인
+ - [x] .NET Framework 4.6.2 x64 Release와 39개 다중 기기 회귀 테스트
+
- [x] v2.0.0 공개 후보 준비
- [x] Windows 버전 2.0.0과 DX Companion 2.0.0(versionCode 6) 일치
- [x] 다중 기기 README·사용 설명서·FAQ·개발 문서 최신화
diff --git a/scripts/Package-Release.ps1 b/scripts/Package-Release.ps1
index 496653d..2b6e4bf 100644
--- a/scripts/Package-Release.ps1
+++ b/scripts/Package-Release.ps1
@@ -183,7 +183,7 @@ if (![string]::Equals(
$actualCompanionHash,
$companionApkSha256,
[StringComparison]::OrdinalIgnoreCase)) {
- throw "The signed DX Companion APK hash does not match the v2.0.0 release candidate."
+ throw "The signed DX Companion APK hash does not match the verified 2.0.0 build."
}
if ([string]::IsNullOrWhiteSpace($Version)) {
From 2a4aff29e58b69d7e825e4106d2365cc552d18ca Mon Sep 17 00:00:00 2001
From: maze-mei
Date: Sun, 23 Aug 2026 17:34:34 +0900
Subject: [PATCH 3/6] Document VirusTotal verification for v2.0.1
---
docs/RELEASE_NOTES_v2.0.1.md | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/docs/RELEASE_NOTES_v2.0.1.md b/docs/RELEASE_NOTES_v2.0.1.md
index 3f0b291..4c0542e 100644
--- a/docs/RELEASE_NOTES_v2.0.1.md
+++ b/docs/RELEASE_NOTES_v2.0.1.md
@@ -38,6 +38,13 @@ executable by itself.
- Bundled `DX-Companion.apk`:
`7CD40017789E22440DCA0291AB0C45ADB564A19D8A623E669F373395536B880F`
+### VirusTotal verification
+
+The published `DXManager.exe` was rescanned after the Microsoft Defender
+false-positive correction. VirusTotal currently reports **0 detections**;
+all listed security vendors, including Microsoft, show **Undetected**:
+https://www.virustotal.com/gui/file/f94c6edc43dedf742e0885e63a8e7d1b0104385b36a29f20d1bc55495ff466eb
+
---
## 한국어
@@ -76,3 +83,10 @@ DX Manager v2.0.1은 v2.0.0의 기기별 설정 도입 뒤 발생한 DeX 설정
- `DXManager.exe`: `F94C6EDC43DEDF742E0885E63A8E7D1B0104385B36A29F20D1BC55495FF466EB`
- 번들 `DX-Companion.apk`:
`7CD40017789E22440DCA0291AB0C45ADB564A19D8A623E669F373395536B880F`
+
+### VirusTotal 검증
+
+Microsoft Defender 오탐 해제 후 게시된 `DXManager.exe`를 다시 검사했습니다.
+VirusTotal은 현재 **탐지 0건**이며 Microsoft를 포함한 전체 보안 엔진이
+**Undetected**로 표시됩니다:
+https://www.virustotal.com/gui/file/f94c6edc43dedf742e0885e63a8e7d1b0104385b36a29f20d1bc55495ff466eb
From b0601afe0fe388bc93a5f9554e48a9d268cde1dd Mon Sep 17 00:00:00 2001
From: tslee
Date: Mon, 31 Aug 2026 12:59:12 +0900
Subject: [PATCH 4/6] feat(mac): add portable arm64 and x64 packages
---
.github/workflows/macos-portable.yml | 181 +++++++
.gitignore | 2 +
.../DexManager.AdbProxy.csproj | 8 +-
DexManager.AdbProxy/Program.cs | 11 +
.../Properties/AssemblyInfo.cs | 9 -
.../Models/ManagedDisplaySession.cs | 1 +
.../Services/DeviceMonitorService.cs | 18 +
DexManager.Core/Services/DexOrchestrator.cs | 488 +++++++++++++++---
DexManager.Mac/DexManager.Mac.csproj | 6 +-
DexManager.Mac/Hosting/InteractiveHost.cs | 387 ++++++++++++--
DexManager.Mac/Platform/MacPathProvider.cs | 53 +-
DexManager.Mac/Platform/MacPlatformService.cs | 2 +-
DexManager.Mac/Program.cs | 58 ++-
DexManager.Tests/MacPathProviderTests.cs | 39 ++
.../licenses/THIRD_PARTY_NOTICES_MACOS.md | 66 +++
README.md | 114 ++--
build/macos/PORTABLE_PACKAGE.txt | 2 +
build/macos/Start DX Manager.command | 28 +
docs/AI_WORKFLOW.md | 18 +
docs/CHANGELOG.md | 19 +-
docs/DECISIONS.md | 42 ++
docs/KNOWN_ISSUES.md | 14 +
docs/MACOS_GUIDE.md | 109 ++--
docs/PACKAGE_README_MACOS.md | 223 ++++++++
docs/PROJECT_BRIEF.md | 6 +
docs/README.md | 1 +
docs/SESSION.md | 34 +-
docs/TODO.md | 12 +
global.json | 7 +
scripts/Package-Mac-Release.sh | 471 +++++++++++++++++
30 files changed, 2187 insertions(+), 242 deletions(-)
create mode 100644 .github/workflows/macos-portable.yml
create mode 100644 DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md
create mode 100644 build/macos/PORTABLE_PACKAGE.txt
create mode 100755 build/macos/Start DX Manager.command
create mode 100644 docs/PACKAGE_README_MACOS.md
create mode 100644 global.json
create mode 100755 scripts/Package-Mac-Release.sh
diff --git a/.github/workflows/macos-portable.yml b/.github/workflows/macos-portable.yml
new file mode 100644
index 0000000..5401f1b
--- /dev/null
+++ b/.github/workflows/macos-portable.yml
@@ -0,0 +1,181 @@
+name: macOS portable packages
+
+on:
+ pull_request:
+ paths:
+ - ".github/workflows/macos-portable.yml"
+ - "build/macos/**"
+ - "DexManager.AdbProxy/**"
+ - "DexManager.Core/**"
+ - "DexManager.Mac/**"
+ - "DexManager.MultiDeviceTests/**"
+ - "DexManager.Tests/**"
+ - "DexManager/licenses/**"
+ - "DexManager/config/README.txt"
+ - "Shared/**"
+ - "DexManager.Mac.sln"
+ - "LICENSE"
+ - "docs/MACOS_GUIDE.md"
+ - "docs/PACKAGE_README_MACOS.md"
+ - "global.json"
+ - "scripts/Package-Mac-Release.sh"
+ push:
+ branches:
+ - main
+ tags:
+ - "v*"
+ paths:
+ - ".github/workflows/macos-portable.yml"
+ - "build/macos/**"
+ - "DexManager.AdbProxy/**"
+ - "DexManager.Core/**"
+ - "DexManager.Mac/**"
+ - "DexManager.MultiDeviceTests/**"
+ - "DexManager.Tests/**"
+ - "DexManager/licenses/**"
+ - "DexManager/config/README.txt"
+ - "Shared/**"
+ - "DexManager.Mac.sln"
+ - "LICENSE"
+ - "docs/MACOS_GUIDE.md"
+ - "docs/PACKAGE_README_MACOS.md"
+ - "global.json"
+ - "scripts/Package-Mac-Release.sh"
+ workflow_dispatch:
+ inputs:
+ version:
+ description: Version used in the generated ZIP filenames
+ required: true
+ default: "2.0.0"
+
+permissions:
+ contents: read
+
+jobs:
+ build-portable:
+ name: ${{ matrix.name }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: Apple Silicon arm64
+ runner: macos-15
+ rid: osx-arm64
+ package_arch: arm64
+ - name: Intel x64
+ runner: macos-15-intel
+ rid: osx-x64
+ package_arch: x64
+
+ runs-on: ${{ matrix.runner }}
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Resolve package version
+ env:
+ REQUESTED_VERSION: ${{ inputs.version }}
+ REF_NAME: ${{ github.ref_name }}
+ REF_TYPE: ${{ github.ref_type }}
+ run: |
+ if [[ "$REF_TYPE" == "tag" ]]; then
+ version="${REF_NAME#v}"
+ elif [[ -n "$REQUESTED_VERSION" ]]; then
+ version="$REQUESTED_VERSION"
+ else
+ version="2.0.0"
+ fi
+ echo "DXM_PACKAGE_VERSION=$version" >> "$GITHUB_ENV"
+
+ - name: Install pinned .NET SDK
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+ with:
+ global-json-file: global.json
+
+ - name: Build with warnings treated as errors
+ run: dotnet build DexManager.Mac.sln --configuration Release --warnaserror --nologo
+
+ - name: Run unit and integration tests
+ run: dotnet test DexManager.Tests/DexManager.Tests.csproj --configuration Release --no-build --nologo
+
+ - name: Run multi-device foundation tests
+ run: dotnet run --project DexManager.MultiDeviceTests/DexManager.MultiDeviceTests.csproj --configuration Release --no-build
+
+ - name: Build and verify portable ZIP
+ run: >-
+ scripts/Package-Mac-Release.sh
+ --rid "${{ matrix.rid }}"
+ --version "$DXM_PACKAGE_VERSION"
+ --skip-tests
+
+ - name: Upload prebuilt portable package
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-${{ matrix.package_arch }}
+ path: |
+ dist/DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-${{ matrix.package_arch }}.zip
+ dist/DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-${{ matrix.package_arch }}.zip.sha256
+ if-no-files-found: error
+ compression-level: 0
+ retention-days: 30
+
+ create-draft-release:
+ name: Create a verified draft GitHub Release
+ if: github.event_name == 'push' && github.ref_type == 'tag'
+ needs:
+ - build-portable
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Download both verified packages
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ pattern: DX-Manager-v*-macos-*
+ path: release-assets
+ merge-multiple: true
+
+ - name: Verify the exact release asset set and checksums
+ env:
+ RELEASE_TAG: ${{ github.ref_name }}
+ run: |
+ version="${RELEASE_TAG#v}"
+ expected=(
+ "DX-Manager-v${version}-macos-arm64.zip"
+ "DX-Manager-v${version}-macos-arm64.zip.sha256"
+ "DX-Manager-v${version}-macos-x64.zip"
+ "DX-Manager-v${version}-macos-x64.zip.sha256"
+ )
+ mapfile -t actual < <(find release-assets -type f -print | sort)
+ if [[ "${#actual[@]}" -ne "${#expected[@]}" ]]; then
+ echo "Expected exactly four release files, found ${#actual[@]}." >&2
+ exit 1
+ fi
+ for filename in "${expected[@]}"; do
+ if [[ ! -f "release-assets/$filename" ]]; then
+ echo "Expected release file is missing: $filename" >&2
+ exit 1
+ fi
+ done
+ (
+ cd release-assets
+ sha256sum --check "DX-Manager-v${version}-macos-arm64.zip.sha256"
+ sha256sum --check "DX-Manager-v${version}-macos-x64.zip.sha256"
+ )
+
+ - name: Create draft release with verified packages
+ env:
+ GH_TOKEN: ${{ github.token }}
+ GH_REPO: ${{ github.repository }}
+ RELEASE_TAG: ${{ github.ref_name }}
+ run: |
+ if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
+ echo "A release already exists for $RELEASE_TAG; refusing to overwrite it." >&2
+ exit 1
+ fi
+ mapfile -t assets < <(find release-assets -type f -print | sort)
+ gh release create "$RELEASE_TAG" "${assets[@]}" \
+ --verify-tag \
+ --draft \
+ --generate-notes \
+ --title "DX Manager $RELEASE_TAG"
diff --git a/.gitignore b/.gitignore
index 1c0f120..66b583b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,5 @@ dist/
# User local settings
**/config/settings.json
+**/config/settings.json.*
+**/*.dSYM/
diff --git a/DexManager.AdbProxy/DexManager.AdbProxy.csproj b/DexManager.AdbProxy/DexManager.AdbProxy.csproj
index 9e77220..cdd6a18 100644
--- a/DexManager.AdbProxy/DexManager.AdbProxy.csproj
+++ b/DexManager.AdbProxy/DexManager.AdbProxy.csproj
@@ -8,7 +8,13 @@
enable
disable
latest
- false
+ DX Manager ADB Proxy
+ ADB compatibility helper for DX Manager
+ maze
+ DX Manager
+ Copyright © 2026 maze
+ 2.0.0
+ false
diff --git a/DexManager.AdbProxy/Program.cs b/DexManager.AdbProxy/Program.cs
index c17fcbe..3f18a23 100644
--- a/DexManager.AdbProxy/Program.cs
+++ b/DexManager.AdbProxy/Program.cs
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.IO.Pipes;
+using System.Reflection;
using DexManager.FileTransfer;
namespace DexManager.AdbProxy;
@@ -12,6 +13,16 @@ private static int Main(string[] args)
{
try
{
+ if (args.Length == 1 && args[0] == "--self-test")
+ {
+ var version = Assembly.GetExecutingAssembly()
+ .GetCustomAttribute()?
+ .InformationalVersion ?? "unknown";
+ Console.WriteLine(
+ $"DX Manager ADB proxy {version} self-test passed.");
+ return 0;
+ }
+
var realAdbPath = Environment.GetEnvironmentVariable(
FileTransferEnvironment.RealAdbPath);
if (string.IsNullOrWhiteSpace(realAdbPath) ||
diff --git a/DexManager.AdbProxy/Properties/AssemblyInfo.cs b/DexManager.AdbProxy/Properties/AssemblyInfo.cs
index cbea1f9..6cb83dc 100644
--- a/DexManager.AdbProxy/Properties/AssemblyInfo.cs
+++ b/DexManager.AdbProxy/Properties/AssemblyInfo.cs
@@ -1,13 +1,4 @@
-using System.Reflection;
using System.Runtime.InteropServices;
-[assembly: AssemblyTitle("DX Manager ADB Proxy")]
-[assembly: AssemblyDescription("ADB compatibility helper for DX Manager")]
-[assembly: AssemblyCompany("maze")]
-[assembly: AssemblyProduct("DX Manager")]
-[assembly: AssemblyCopyright("Copyright © 2026 maze")]
[assembly: ComVisible(false)]
[assembly: Guid("3a5433aa-c5ec-4741-89d7-a8a4934d5038")]
-[assembly: AssemblyVersion("1.3.0.0")]
-[assembly: AssemblyFileVersion("1.3.0.0")]
-[assembly: AssemblyInformationalVersion("1.3.0")]
diff --git a/DexManager.Core/Models/ManagedDisplaySession.cs b/DexManager.Core/Models/ManagedDisplaySession.cs
index 1f4a4bc..f96a8f4 100644
--- a/DexManager.Core/Models/ManagedDisplaySession.cs
+++ b/DexManager.Core/Models/ManagedDisplaySession.cs
@@ -4,6 +4,7 @@ public sealed class ManagedDisplaySession
{
public string Mode { get; set; }
public string Serial { get; set; }
+ public string DeviceIdentity { get; set; }
public string AppPackage { get; set; }
public int DisplayId { get; set; }
public int ScrcpyProcessId { get; set; }
diff --git a/DexManager.Core/Services/DeviceMonitorService.cs b/DexManager.Core/Services/DeviceMonitorService.cs
index a8bda52..773c8ac 100644
--- a/DexManager.Core/Services/DeviceMonitorService.cs
+++ b/DexManager.Core/Services/DeviceMonitorService.cs
@@ -15,6 +15,8 @@ public sealed class DeviceMonitorService : IDisposable
private readonly int _disconnectConfirmationMs;
private readonly object _stateLock = new object();
private readonly object _lifecycleLock = new object();
+ private readonly ManualResetEventSlim _firstPollCompleted =
+ new ManualResetEventSlim(false);
private readonly Dictionary _deviceNameCache =
new Dictionary(
StringComparer.OrdinalIgnoreCase);
@@ -78,6 +80,7 @@ public void Start()
if (_timer != null) return;
Interlocked.Exchange(ref _stopping, 0);
_missingSinceUtc = DateTime.MinValue;
+ _firstPollCompleted.Reset();
_timer = new Timer(Poll, null, 0, _intervalMs);
_logService.Info(LocalizationService.Get(
"Log.DeviceMonitor.Started"));
@@ -97,11 +100,21 @@ public void RequestShutdown()
lock (_lifecycleLock)
{
Interlocked.Exchange(ref _stopping, 1);
+ _firstPollCompleted.Set();
if (_timer != null)
_timer.Change(Timeout.Infinite, Timeout.Infinite);
}
}
+ public bool WaitForFirstPoll(
+ int timeoutMs,
+ CancellationToken cancellationToken)
+ {
+ return _firstPollCompleted.Wait(
+ Math.Max(0, timeoutMs),
+ cancellationToken);
+ }
+
public void Dispose()
{
lock (_lifecycleLock)
@@ -114,6 +127,7 @@ public void Dispose()
private void StopTimer()
{
Interlocked.Exchange(ref _stopping, 1);
+ _firstPollCompleted.Set();
var timer = Interlocked.Exchange(ref _timer, null);
if (timer == null) return;
@@ -130,6 +144,7 @@ private void Poll(object state)
if (IsStopping ||
Interlocked.Exchange(ref _polling, 1) == 1) return;
+ var snapshotCompleted = false;
try
{
IList devices;
@@ -141,6 +156,7 @@ private void Poll(object state)
RefreshVisibleDevices(devices);
ReconcilePhysicalDevices(devices);
+ snapshotCompleted = true;
var eligibleDevices = GetEligibleDevices(devices);
var preferred = _wirelessAdbService.FindPreferredDevice(
eligibleDevices,
@@ -219,6 +235,8 @@ private void Poll(object state)
finally
{
Interlocked.Exchange(ref _polling, 0);
+ if (snapshotCompleted)
+ _firstPollCompleted.Set();
}
}
diff --git a/DexManager.Core/Services/DexOrchestrator.cs b/DexManager.Core/Services/DexOrchestrator.cs
index 87d752c..15f3cd8 100644
--- a/DexManager.Core/Services/DexOrchestrator.cs
+++ b/DexManager.Core/Services/DexOrchestrator.cs
@@ -23,11 +23,9 @@ public sealed class DexOrchestrator
private int _shutdownRequested;
private Task _shutdownTask;
private ManagedDisplaySession _currentSession;
- private readonly Dictionary
+ private readonly List
_pendingDisplayCleanup =
- new Dictionary(
- StringComparer.OrdinalIgnoreCase);
- private int _naturalExitCleanupScheduled;
+ new List();
public DexOrchestrator(
AdbService adbService,
@@ -62,7 +60,29 @@ public ManagedDisplaySession CurrentSession
get { return _currentSession; }
}
- public string DeviceIdentity { get; set; }
+ public bool HasDeferredDisplayCleanup
+ {
+ get
+ {
+ lock (_operationGate)
+ {
+ return _pendingDisplayCleanup.Count > 0;
+ }
+ }
+ }
+
+ public bool IsCleanupComplete
+ {
+ get
+ {
+ lock (_operationGate)
+ {
+ return !_scrcpyService.IsRunning &&
+ _currentSession == null &&
+ _pendingDisplayCleanup.Count == 0;
+ }
+ }
+ }
public bool IsShutdownRequested
{
@@ -75,12 +95,25 @@ public bool IsShutdownRequested
}
}
- public Task StartAsync(string serial)
+ public async Task StartAsync(
+ string serial,
+ string deviceIdentity,
+ CancellationToken cancellationToken)
{
- return Task.Run(delegate
+ using (cancellationToken.Register(RequestShutdown))
{
- lock (_operationGate) StartCore(serial);
- });
+ var started = await Task.Run(delegate
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ lock (_operationGate)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return StartCore(serial, deviceIdentity);
+ }
+ }, cancellationToken).ConfigureAwait(false);
+ cancellationToken.ThrowIfCancellationRequested();
+ return started;
+ }
}
public Task StopAsync()
@@ -91,12 +124,55 @@ public Task StopAsync()
});
}
- public Task RetryDeferredCleanupAsync(string serial)
+ public Task StopOrConfirmCleanupAsync()
{
return Task.Run(delegate
{
lock (_operationGate)
- return RetryDeferredCleanupCore(serial);
+ {
+ if (!_scrcpyService.IsRunning &&
+ _currentSession == null)
+ {
+ return _pendingDisplayCleanup.Count == 0;
+ }
+ StopCore();
+ return _pendingDisplayCleanup.Count == 0;
+ }
+ });
+ }
+
+ public Task RetryDeferredCleanupAsync(
+ string serial,
+ string deviceIdentity)
+ {
+ return Task.Run(delegate
+ {
+ lock (_operationGate)
+ return RetryDeferredCleanupCore(
+ serial,
+ deviceIdentity);
+ });
+ }
+
+ public Task CleanupConnectedOverlayAsync(
+ string serial,
+ string deviceIdentity)
+ {
+ return Task.Run(delegate
+ {
+ lock (_operationGate)
+ {
+ string verifiedIdentity;
+ if (!CleanupConnectedTargetOverlay(
+ serial,
+ deviceIdentity,
+ out verifiedIdentity))
+ return false;
+ CompleteDeferredCleanupCore(
+ serial,
+ verifiedIdentity);
+ return true;
+ }
});
}
@@ -107,7 +183,9 @@ public void RequestShutdown()
_shutdownSignal.Set();
}
- public Task ShutdownAsync(string fallbackSerial)
+ public Task ShutdownAsync(
+ string fallbackSerial,
+ string fallbackIdentity)
{
RequestShutdown();
lock (_shutdownTaskLock)
@@ -119,7 +197,9 @@ public Task ShutdownAsync(string fallbackSerial)
_shutdownTask = Task.Run(delegate
{
lock (_operationGate)
- ShutdownCore(fallbackSerial);
+ ShutdownCore(
+ fallbackSerial,
+ fallbackIdentity);
});
}
return _shutdownTask;
@@ -135,14 +215,16 @@ public Task ApplyRuntimeSettingsAsync()
});
}
- private void StartCore(string requestedSerial)
+ private bool StartCore(
+ string requestedSerial,
+ string deviceIdentity)
{
- if (IsShutdownRequested) return;
+ if (IsShutdownRequested) return false;
if (_scrcpyService.IsRunning)
{
_logService.Warning(LocalizationService.Get(
"Log.Dex.AlreadyRunning"));
- return;
+ return false;
}
var serial = requestedSerial;
@@ -153,14 +235,17 @@ private void StartCore(string requestedSerial)
LocalizationService.Get(
"Error.Dex.NoAuthorizedDevice"));
}
- if (!RetryDeferredCleanupCore(serial))
+ deviceIdentity = GetVerifiedDeviceIdentity(
+ serial,
+ deviceIdentity);
+ if (string.IsNullOrWhiteSpace(deviceIdentity))
{
throw new InvalidOperationException(
- LocalizationService.Get(
- "Error.Dex.DisplayResetFailed"));
+ "The physical-device identity could not be verified. " +
+ "Keep the phone connected and unlocked, then try again.");
}
- CleanupStaleSession(serial);
- var runSettings = GetDeviceRunSettings();
+ CleanupStaleSession(serial, deviceIdentity);
+ var runSettings = GetDeviceRunSettings(deviceIdentity);
VirtualDisplayLease lease = null;
var scrcpyStarted = false;
@@ -174,6 +259,9 @@ private void StartCore(string requestedSerial)
runSettings.VirtualDisplay,
_settings.Timing.VirtualDisplayDetectionTimeoutMs,
delegate { return IsShutdownRequested; });
+ CompleteDeferredCleanupCore(
+ serial,
+ deviceIdentity);
ThrowIfShutdownRequested();
_scrcpyService.Start(
runSettings.Scrcpy,
@@ -190,14 +278,21 @@ private void StartCore(string requestedSerial)
LocalizationService.Get(
"Error.Scrcpy.ExitedBeforeWindow"));
- TrackSession("DeX", serial, lease);
+ TrackSession(
+ "DeX",
+ serial,
+ deviceIdentity,
+ lease);
if (!_scrcpyService.IsRunning)
throw new InvalidOperationException(
LocalizationService.Get(
"Error.Scrcpy.ExitedBeforeWindow"));
try
{
- SaveLastSuccess(serial, lease.DisplayId);
+ SaveLastSuccess(
+ serial,
+ deviceIdentity,
+ lease.DisplayId);
}
catch (Exception saveException)
{
@@ -208,18 +303,26 @@ private void StartCore(string requestedSerial)
}
_logService.Info(LocalizationService.Get(
"Log.Dex.StartCompleted"));
+ return true;
}
catch (OperationCanceledException ex)
{
lease = GetRetainedLease(ex, lease);
- CleanupFailedStart(scrcpyStarted, lease);
+ CleanupFailedStart(
+ scrcpyStarted,
+ lease,
+ deviceIdentity);
_logService.Info(LocalizationService.Get(
"Log.Dex.StartCancelled"));
+ throw;
}
catch (Exception ex)
{
lease = GetRetainedLease(ex, lease);
- CleanupFailedStart(scrcpyStarted, lease);
+ CleanupFailedStart(
+ scrcpyStarted,
+ lease,
+ deviceIdentity);
_logService.Error(
LocalizationService.Get("Log.Dex.StartFailed"),
ex);
@@ -254,7 +357,9 @@ private void StopCore()
if (session != null)
{
- if (!ReleaseDisplayLease(session.DisplayLease))
+ if (!ReleaseDisplayLease(
+ session.DisplayLease,
+ session.DeviceIdentity))
DeferDisplayCleanup(session);
}
ClearSession(session);
@@ -284,7 +389,9 @@ private bool ApplyRuntimeSettingsCore()
var session = _currentSession;
_scrcpyService.Stop();
if (session != null &&
- !ReleaseDisplayLease(session.DisplayLease))
+ !ReleaseDisplayLease(
+ session.DisplayLease,
+ session.DeviceIdentity))
{
DeferDisplayCleanup(session);
throw new InvalidOperationException(
@@ -294,7 +401,10 @@ private bool ApplyRuntimeSettingsCore()
ClearSession(session);
if (_shutdownSignal.WaitOne(1000)) return false;
- StartCore(serial);
+ var deviceIdentity = session == null
+ ? string.Empty
+ : session.DeviceIdentity;
+ if (!StartCore(serial, deviceIdentity)) return false;
if (!_scrcpyService.IsRunning) return false;
_logService.Info(LocalizationService.Get(
"Log.Dex.ApplyCompleted"));
@@ -309,7 +419,9 @@ private bool ApplyRuntimeSettingsCore()
}
}
- private void ShutdownCore(string fallbackSerial)
+ private void ShutdownCore(
+ string fallbackSerial,
+ string fallbackIdentity)
{
var session = _currentSession;
Exception stopException = null;
@@ -335,17 +447,32 @@ private void ShutdownCore(string fallbackSerial)
}
if (session != null &&
- !ReleaseDisplayLease(session.DisplayLease))
+ !ReleaseDisplayLease(
+ session.DisplayLease,
+ session.DeviceIdentity))
{
DeferDisplayCleanup(session);
}
else if (session == null)
{
- CleanupConnectedTargetOverlay(fallbackSerial);
+ string verifiedIdentity;
+ if (!CleanupConnectedTargetOverlay(
+ fallbackSerial,
+ fallbackIdentity,
+ out verifiedIdentity))
+ {
+ throw new InvalidOperationException(
+ LocalizationService.Get(
+ "Error.Dex.DisplayResetFailed"));
+ }
+ CompleteDeferredCleanupCore(
+ fallbackSerial,
+ verifiedIdentity);
}
ClearSession(session);
_logService.Info(LocalizationService.Get(
"Log.Dex.ShutdownCleanupCompleted"));
+ if (stopException != null) throw stopException;
}
private void ScrcpyService_RunningChanged(
@@ -353,25 +480,12 @@ private void ScrcpyService_RunningChanged(
EventArgs e)
{
if (_scrcpyService.IsRunning) return;
- if (Interlocked.Exchange(
- ref _naturalExitCleanupScheduled,
- 1) != 0) return;
-
Task.Run(delegate
{
- try
- {
- lock (_operationGate)
- {
- if (!_scrcpyService.IsRunning)
- CleanupNaturallyEndedSession();
- }
- }
- finally
+ lock (_operationGate)
{
- Interlocked.Exchange(
- ref _naturalExitCleanupScheduled,
- 0);
+ if (!_scrcpyService.IsRunning)
+ CleanupNaturallyEndedSession();
}
});
}
@@ -380,7 +494,9 @@ private void CleanupNaturallyEndedSession()
{
var session = _currentSession;
if (session == null) return;
- if (!ReleaseDisplayLease(session.DisplayLease))
+ if (!ReleaseDisplayLease(
+ session.DisplayLease,
+ session.DeviceIdentity))
{
DeferDisplayCleanup(session);
_logService.Warning(LocalizationService.Get(
@@ -393,22 +509,30 @@ private void CleanupNaturallyEndedSession()
"Log.Dex.NaturalExitCleanupCompleted"));
}
- private void CleanupStaleSession(string nextSerial)
+ private void CleanupStaleSession(
+ string nextSerial,
+ string nextDeviceIdentity)
{
var stale = _currentSession;
if (stale == null) return;
if (string.Equals(
stale.Serial,
nextSerial,
- StringComparison.OrdinalIgnoreCase))
+ StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(
+ stale.DeviceIdentity,
+ nextDeviceIdentity,
+ StringComparison.OrdinalIgnoreCase))
{
// A reconnect must be evaluated by EnsureVirtualDisplay.
- // Releasing the stale lease here would always delete a
- // perfectly reusable overlay before the comparison.
- ClearSession(stale);
+ // Preserve the cleanup evidence until that comparison and
+ // any required recreation have actually succeeded.
+ DeferDisplayCleanup(stale);
return;
}
- if (ReleaseDisplayLease(stale.DisplayLease))
+ if (ReleaseDisplayLease(
+ stale.DisplayLease,
+ stale.DeviceIdentity))
{
ClearSession(stale);
return;
@@ -428,7 +552,8 @@ private void CleanupStaleSession(string nextSerial)
private void CleanupFailedStart(
bool scrcpyStarted,
- VirtualDisplayLease lease)
+ VirtualDisplayLease lease,
+ string deviceIdentity)
{
if (scrcpyStarted || _scrcpyService.IsRunning)
{
@@ -448,16 +573,24 @@ private void CleanupFailedStart(
if (_scrcpyService.IsRunning)
{
if (_currentSession == null && lease != null)
- TrackSession("DeX", lease.Serial, lease);
+ TrackSession(
+ "DeX",
+ lease.Serial,
+ deviceIdentity,
+ lease);
return;
}
try
{
if (lease != null &&
- !ReleaseDisplayLease(lease))
+ !ReleaseDisplayLease(
+ lease,
+ deviceIdentity))
{
- DeferDisplayCleanup(lease);
+ DeferDisplayCleanup(
+ lease,
+ deviceIdentity);
ClearSession(_currentSession);
return;
}
@@ -472,60 +605,234 @@ private void CleanupFailedStart(
}
}
- private bool RetryDeferredCleanupCore(string serial)
+ private bool RetryDeferredCleanupCore(
+ string serial,
+ string deviceIdentity)
{
if (string.IsNullOrWhiteSpace(serial)) return true;
- if (!_pendingDisplayCleanup.ContainsKey(serial))
- return true;
- // The device is available again. Do not delete the old overlay
- // merely because the previous connection ended unexpectedly;
- // the next start will compare its actual resolution and DPI.
- _pendingDisplayCleanup.Remove(serial);
+ var verifiedIdentity = GetVerifiedDeviceIdentity(
+ serial,
+ deviceIdentity);
+ // Keep the entry until EnsureVirtualDisplay or an explicit Reset
+ // succeeds for this verified physical device.
+ return !string.IsNullOrWhiteSpace(verifiedIdentity);
+ }
+
+ private void CompleteDeferredCleanupCore(
+ string serial,
+ string deviceIdentity)
+ {
+ var pendingEntries = GetMatchingDeferredCleanupEntries(
+ serial,
+ deviceIdentity);
+ if (pendingEntries.Count == 0) return;
+ RemoveDeferredCleanupEntries(pendingEntries);
_logService.Info(LocalizationService.Format(
- "Log.Dex.DeferredCleanupSuperseded",
+ "Log.Dex.DeferredCleanupCompleted",
serial));
- return true;
}
- private bool ReleaseDisplayLease(VirtualDisplayLease lease)
+ private List
+ GetMatchingDeferredCleanupEntries(
+ string serial,
+ string deviceIdentity)
+ {
+ var matches = new List();
+ if (!HasStableIdentity(deviceIdentity)) return matches;
+ foreach (var pending in _pendingDisplayCleanup)
+ {
+ if (HasStableIdentity(pending.DeviceIdentity) &&
+ string.Equals(
+ pending.DeviceIdentity,
+ deviceIdentity,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ matches.Add(pending);
+ }
+ }
+ return matches;
+ }
+
+ private void RemoveDeferredCleanupEntries(
+ IList entries)
+ {
+ for (var index = 0; index < entries.Count; index++)
+ _pendingDisplayCleanup.Remove(entries[index]);
+ }
+
+ private static bool HasStableIdentity(string identity)
+ {
+ return !string.IsNullOrWhiteSpace(identity) &&
+ !PhysicalDeviceRegistry.IsTemporaryIdentity(identity);
+ }
+
+ private bool ReleaseDisplayLease(
+ VirtualDisplayLease lease,
+ string expectedDeviceIdentity)
{
if (lease == null) return true;
- if (string.IsNullOrWhiteSpace(lease.Serial) ||
- !_adbService.IsAuthorizedDeviceConnected(lease.Serial))
+ string verifiedIdentity;
+ var cleanupSerial = FindVerifiedCleanupTransport(
+ lease.Serial,
+ expectedDeviceIdentity,
+ out verifiedIdentity);
+ if (string.IsNullOrWhiteSpace(cleanupSerial))
+ return false;
+
+ if (string.Equals(
+ cleanupSerial,
+ lease.Serial,
+ StringComparison.OrdinalIgnoreCase))
{
+ return _virtualDisplayService.Release(lease);
+ }
+
+ var reset = _virtualDisplayService.Reset(cleanupSerial);
+ if (reset) lease.OwnsOverlaySetting = false;
+ return reset;
+ }
+
+ private bool CleanupConnectedTargetOverlay(
+ string serial,
+ string expectedDeviceIdentity,
+ out string verifiedDeviceIdentity)
+ {
+ var cleanupSerial = FindVerifiedCleanupTransport(
+ serial,
+ expectedDeviceIdentity,
+ out verifiedDeviceIdentity);
+ if (string.IsNullOrWhiteSpace(cleanupSerial))
return false;
+
+ return _virtualDisplayService.Reset(cleanupSerial);
+ }
+
+ private string FindVerifiedCleanupTransport(
+ string preferredSerial,
+ string expectedDeviceIdentity,
+ out string verifiedDeviceIdentity)
+ {
+ verifiedDeviceIdentity = GetVerifiedDeviceIdentity(
+ preferredSerial,
+ expectedDeviceIdentity);
+ if (!string.IsNullOrWhiteSpace(verifiedDeviceIdentity))
+ return preferredSerial;
+
+ if (!HasStableIdentity(expectedDeviceIdentity))
+ return string.Empty;
+
+ IList devices;
+ if (!_adbService.TryGetDevices(false, out devices) ||
+ devices == null)
+ {
+ return string.Empty;
+ }
+
+ foreach (var device in devices)
+ {
+ if (device == null ||
+ device.Status != AdbDeviceStatus.Device ||
+ string.IsNullOrWhiteSpace(device.Serial) ||
+ string.Equals(
+ device.Serial,
+ preferredSerial,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ var liveIdentity = _adbService.GetDeviceIdentity(
+ device.Serial);
+ if (!string.Equals(
+ liveIdentity,
+ expectedDeviceIdentity,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ verifiedDeviceIdentity = liveIdentity;
+ return device.Serial;
}
- return _virtualDisplayService.Release(lease);
+ return string.Empty;
}
- private void CleanupConnectedTargetOverlay(string serial)
+ private string GetVerifiedDeviceIdentity(
+ string serial,
+ string expectedDeviceIdentity)
{
if (string.IsNullOrWhiteSpace(serial) ||
!_adbService.IsAuthorizedDeviceConnected(serial))
{
- return;
+ return string.Empty;
}
- _virtualDisplayService.Reset(serial);
+ var liveIdentity = _adbService.GetDeviceIdentity(serial);
+ if (!HasStableIdentity(liveIdentity)) return string.Empty;
+ if (HasStableIdentity(expectedDeviceIdentity) &&
+ !string.Equals(
+ expectedDeviceIdentity,
+ liveIdentity,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Empty;
+ }
+ return liveIdentity;
}
private void DeferDisplayCleanup(ManagedDisplaySession session)
{
if (session == null) return;
- DeferDisplayCleanup(session.DisplayLease);
+ DeferDisplayCleanup(
+ session.DisplayLease,
+ session.DeviceIdentity);
ClearSession(session);
}
- private void DeferDisplayCleanup(VirtualDisplayLease lease)
+ private void DeferDisplayCleanup(
+ VirtualDisplayLease lease,
+ string deviceIdentity)
{
if (lease == null || string.IsNullOrWhiteSpace(lease.Serial))
return;
- _pendingDisplayCleanup[lease.Serial] = lease;
+ var normalizedIdentity = deviceIdentity ?? string.Empty;
+ for (var index = 0;
+ index < _pendingDisplayCleanup.Count;
+ index++)
+ {
+ var pending = _pendingDisplayCleanup[index];
+ if (string.Equals(
+ pending.Lease.Serial,
+ lease.Serial,
+ StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(
+ pending.DeviceIdentity,
+ normalizedIdentity,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ pending.Lease = lease;
+ _logService.Warning(LocalizationService.Format(
+ "Log.Dex.DeferredCleanupStored",
+ lease.Serial));
+ return;
+ }
+ }
+ _pendingDisplayCleanup.Add(new DeferredDisplayCleanup
+ {
+ DeviceIdentity = normalizedIdentity,
+ Lease = lease
+ });
_logService.Warning(LocalizationService.Format(
"Log.Dex.DeferredCleanupStored",
lease.Serial));
}
+ private sealed class DeferredDisplayCleanup
+ {
+ public string DeviceIdentity { get; set; }
+ public VirtualDisplayLease Lease { get; set; }
+ }
+
private static VirtualDisplayLease GetRetainedLease(
Exception error,
VirtualDisplayLease current)
@@ -541,12 +848,17 @@ private void ThrowIfShutdownRequested()
throw new OperationCanceledException();
}
- private void SaveLastSuccess(string serial, int displayId)
+ private void SaveLastSuccess(
+ string serial,
+ string deviceIdentity,
+ int displayId)
{
_settingsService.UpdateAndSave(_settings, delegate(
AppSettings settings)
{
- var runSettings = GetDeviceRunSettings(settings);
+ var runSettings = GetDeviceRunSettings(
+ settings,
+ deviceIdentity);
runSettings.LastSuccess.Width =
runSettings.VirtualDisplay.Width;
runSettings.LastSuccess.Height =
@@ -570,13 +882,15 @@ private void SaveLastSuccess(string serial, int displayId)
private void TrackSession(
string mode,
string serial,
+ string deviceIdentity,
VirtualDisplayLease lease)
{
_currentSession = new ManagedDisplaySession
{
Mode = mode,
Serial = serial,
- AppPackage = GetDeviceRunSettings()
+ DeviceIdentity = deviceIdentity ?? string.Empty,
+ AppPackage = GetDeviceRunSettings(deviceIdentity)
.Scrcpy.StartAppPackage,
DisplayId = lease.DisplayId,
ScrcpyProcessId = _scrcpyService.CurrentProcessId,
@@ -589,17 +903,21 @@ private void TrackSession(
_currentSession));
}
- private DeviceRunSettingsProfile GetDeviceRunSettings()
+ private DeviceRunSettingsProfile GetDeviceRunSettings(
+ string deviceIdentity)
{
- return GetDeviceRunSettings(_settings);
+ return GetDeviceRunSettings(
+ _settings,
+ deviceIdentity);
}
private DeviceRunSettingsProfile GetDeviceRunSettings(
- AppSettings settings)
+ AppSettings settings,
+ string deviceIdentity)
{
- if (!string.IsNullOrWhiteSpace(DeviceIdentity))
+ if (!string.IsNullOrWhiteSpace(deviceIdentity))
return settings.GetOrCreateDeviceRunSettings(
- DeviceIdentity);
+ deviceIdentity);
return new DeviceRunSettingsProfile
{
DeviceIdentity = string.Empty,
diff --git a/DexManager.Mac/DexManager.Mac.csproj b/DexManager.Mac/DexManager.Mac.csproj
index e5a03c0..f22ec2b 100644
--- a/DexManager.Mac/DexManager.Mac.csproj
+++ b/DexManager.Mac/DexManager.Mac.csproj
@@ -9,6 +9,9 @@
disable
latest
true
+ 2.0.0
+ false
+ true
@@ -16,9 +19,10 @@
-
+
tools\%(RecursiveDir)%(Filename)%(Extension)
PreserveNewest
+ PreserveNewest
diff --git a/DexManager.Mac/Hosting/InteractiveHost.cs b/DexManager.Mac/Hosting/InteractiveHost.cs
index 454cf37..e396766 100644
--- a/DexManager.Mac/Hosting/InteractiveHost.cs
+++ b/DexManager.Mac/Hosting/InteractiveHost.cs
@@ -31,8 +31,11 @@ public sealed class InteractiveHost : IDisposable
private DeviceRuntimeServiceFactory _runtimeFactory;
private DeviceRuntimeServiceSet _activeRuntime;
private string _selectedDeviceSerial;
+ private string _selectedDeviceIdentity;
private bool _isRunning;
private bool _disposed;
+ private int _shutdownStarted;
+ private int _runtimeServicesDisposed;
public InteractiveHost()
{
@@ -120,7 +123,12 @@ private void EnsureDefaultPaths()
{
var modified = false;
var currentAdb = _settings.Paths.AdbPath ?? string.Empty;
- if (string.IsNullOrWhiteSpace(currentAdb) || !File.Exists(currentAdb) || currentAdb.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
+ var forcePortableAdb = _pathProvider.IsPortablePackage &&
+ _settings.Paths.AdbSelectionMode != AdbSelectionMode.Manual;
+ if (forcePortableAdb ||
+ string.IsNullOrWhiteSpace(currentAdb) ||
+ !File.Exists(currentAdb) ||
+ currentAdb.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
var adb = _pathProvider.ResolveDefaultAdbPath();
if (File.Exists(adb))
@@ -131,7 +139,10 @@ private void EnsureDefaultPaths()
}
var currentScrcpy = _settings.Paths.ScrcpyPath ?? string.Empty;
- if (string.IsNullOrWhiteSpace(currentScrcpy) || !File.Exists(currentScrcpy) || currentScrcpy.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
+ if (_pathProvider.IsPortablePackage ||
+ string.IsNullOrWhiteSpace(currentScrcpy) ||
+ !File.Exists(currentScrcpy) ||
+ currentScrcpy.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
var scrcpy = _pathProvider.ResolveDefaultScrcpyPath();
if (File.Exists(scrcpy))
@@ -211,10 +222,12 @@ public async Task RunAsync(CancellationToken cancellationToken = default)
switch (input.ToUpperInvariant())
{
case "1":
- await StartDexAsync();
+ await StartDexAsync(cancellationToken);
break;
case "2":
- await StopDexAsync();
+ await StopDexAsync(
+ cleanupUntrackedOverlay: true,
+ cancellationToken: cancellationToken);
break;
case "3":
await StartSingleWindowAsync();
@@ -229,7 +242,7 @@ public async Task RunAsync(CancellationToken cancellationToken = default)
await ManageFileTransferAsync();
break;
case "7":
- await RunDiagnosticsAsync();
+ await RunDiagnosticsAsync(cancellationToken);
break;
case "8":
await ManageCompanionAppAsync();
@@ -260,7 +273,7 @@ public async Task RunAsync(CancellationToken cancellationToken = default)
}
}
- Shutdown();
+ await ShutdownAsync();
}
private void PrintBanner()
@@ -318,6 +331,20 @@ private void PrintDashboard()
private PhysicalDeviceInfo GetSelectedDevice()
{
var snapshot = _deviceRegistry.Current;
+ if (!string.IsNullOrWhiteSpace(_selectedDeviceIdentity))
+ {
+ var identityMatch = snapshot.Devices.FirstOrDefault(d =>
+ string.Equals(
+ d.Identity,
+ _selectedDeviceIdentity,
+ StringComparison.OrdinalIgnoreCase));
+ if (identityMatch != null) return identityMatch;
+ if (!PhysicalDeviceRegistry.IsTemporaryIdentity(
+ _selectedDeviceIdentity))
+ {
+ return null;
+ }
+ }
if (string.IsNullOrWhiteSpace(_selectedDeviceSerial))
{
return snapshot.Devices.FirstOrDefault();
@@ -343,6 +370,55 @@ private DeviceRuntimeServiceSet GetOrCreateRuntime()
return _activeRuntime;
}
+ public bool IsDexRunning =>
+ _activeRuntime?.Dex.IsRunning == true;
+
+ public bool IsDexCleanupComplete
+ {
+ get
+ {
+ var runtime = _activeRuntime;
+ return runtime == null || runtime.Dex.IsCleanupComplete;
+ }
+ }
+
+ public async Task WaitForDexCleanupAsync(
+ CancellationToken cancellationToken = default)
+ {
+ var runtime = _activeRuntime;
+ if (runtime == null) return true;
+
+ var timeoutMs = (int)Math.Min(
+ 60000L,
+ Math.Max(
+ 15000L,
+ (long)_settings.Timing.ProcessTimeoutMs * 2L));
+ var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
+ var nextRetryUtc = DateTime.MinValue;
+ while (!runtime.Dex.IsCleanupComplete &&
+ DateTime.UtcNow < deadline)
+ {
+ if (!runtime.Dex.IsRunning &&
+ runtime.Dex.CurrentSession == null &&
+ runtime.Dex.HasDeferredDisplayCleanup &&
+ DateTime.UtcNow >= nextRetryUtc)
+ {
+ nextRetryUtc = DateTime.UtcNow.AddSeconds(1);
+ var device = GetSelectedDevice();
+ var serial = GetPrimarySerial(device);
+ if (device != null &&
+ !string.IsNullOrWhiteSpace(serial))
+ {
+ await runtime.Dex.CleanupConnectedOverlayAsync(
+ serial,
+ device.Identity);
+ }
+ }
+ await Task.Delay(100, cancellationToken);
+ }
+ return runtime.Dex.IsCleanupComplete;
+ }
+
private void SelectTargetDevice()
{
var snapshot = _deviceRegistry.Current;
@@ -366,59 +442,145 @@ private void SelectTargetDevice()
{
var target = snapshot.Devices[idx - 1];
_selectedDeviceSerial = GetPrimarySerial(target);
+ _selectedDeviceIdentity = target.Identity;
AnsiConsole.Success($"Selected device: {target.DisplayName}");
}
}
- public async Task StartDexAsync()
+ public async Task StartDexAsync(
+ CancellationToken cancellationToken = default)
{
+ await WaitForDeviceSnapshotAsync(cancellationToken);
var device = GetSelectedDevice();
var serial = GetPrimarySerial(device);
if (string.IsNullOrWhiteSpace(serial))
{
AnsiConsole.Error("No device connected. Please connect a Galaxy device.");
- return;
+ return false;
}
-
+ _selectedDeviceSerial = serial;
+ _selectedDeviceIdentity = device.Identity;
AnsiConsole.Header($"STARTING DeX ON {device.DisplayName}");
var runtime = GetOrCreateRuntime();
try
{
AnsiConsole.Info("Configuring overlay display resolution and launching scrcpy...");
- await runtime.Dex.StartAsync(serial);
+ if (!await runtime.Dex.StartAsync(
+ serial,
+ device.Identity,
+ cancellationToken))
+ {
+ AnsiConsole.Warning(
+ "A new DeX session was not started. " +
+ "Stop the current session before choosing another device.");
+ await Task.Delay(1000);
+ return false;
+ }
+ _selectedDeviceSerial =
+ runtime.Dex.CurrentSession?.Serial ?? serial;
+ _selectedDeviceIdentity =
+ runtime.Dex.CurrentSession?.DeviceIdentity ??
+ device.Identity;
AnsiConsole.Success("DeX successfully started!");
+ await Task.Delay(1000);
+ return true;
+ }
+ catch (OperationCanceledException)
+ {
+ AnsiConsole.Info("DeX launch was cancelled; cleanup is in progress.");
+ throw;
}
catch (Exception ex)
{
AnsiConsole.Error($"DeX launch error: {ex.Message}");
+ await Task.Delay(1000);
+ return false;
}
-
- await Task.Delay(1000);
}
- public async Task StopDexAsync()
+ public async Task StopDexAsync(
+ bool cleanupUntrackedOverlay = false,
+ CancellationToken cancellationToken = default)
{
- var device = GetSelectedDevice();
- if (device == null)
+ var runtime = _activeRuntime;
+ var stopKnownRuntime = !cleanupUntrackedOverlay;
+ PhysicalDeviceInfo device = null;
+ string serial = null;
+
+ if (stopKnownRuntime && runtime == null)
{
- AnsiConsole.Warning("No target device selected.");
- return;
+ AnsiConsole.Warning("No active DeX session is tracked.");
+ return false;
}
- AnsiConsole.Info($"Stopping DeX on {device.DisplayName}...");
- var runtime = GetOrCreateRuntime();
+ if (cleanupUntrackedOverlay)
+ {
+ stopKnownRuntime = runtime != null &&
+ (runtime.Dex.CurrentSession != null ||
+ runtime.Dex.IsRunning);
+ }
+
+ if (!stopKnownRuntime)
+ {
+ await WaitForDeviceSnapshotAsync(cancellationToken);
+ device = GetSelectedDevice();
+ if (device == null)
+ {
+ AnsiConsole.Warning("No target device selected.");
+ return false;
+ }
+
+ serial = GetPrimarySerial(device);
+ if (string.IsNullOrWhiteSpace(serial) ||
+ !_adbService.IsAuthorizedDeviceConnected(serial))
+ {
+ AnsiConsole.Warning(
+ "The target device is not connected and authorized; " +
+ "DeX display cleanup was not attempted.");
+ return false;
+ }
+
+ runtime = GetOrCreateRuntime();
+ }
+
+ var targetName = device?.DisplayName ??
+ runtime.Dex.CurrentSession?.Serial ??
+ "the selected device";
+ AnsiConsole.Info($"Stopping DeX on {targetName}...");
try
{
- await runtime.Dex.StopAsync();
+ if (stopKnownRuntime)
+ {
+ if (!await runtime.Dex.StopOrConfirmCleanupAsync())
+ {
+ AnsiConsole.Warning(
+ "DeX stopped, but display cleanup was deferred until " +
+ "the target device reconnects.");
+ await Task.Delay(800);
+ return false;
+ }
+ }
+ else
+ {
+ if (!await runtime.Dex.CleanupConnectedOverlayAsync(
+ serial,
+ device.Identity))
+ {
+ throw new InvalidOperationException(
+ "The DeX display overlay could not be removed.");
+ }
+ }
AnsiConsole.Success("DeX session stopped and display overlay cleaned up.");
+ await Task.Delay(800);
+ return true;
}
catch (Exception ex)
{
AnsiConsole.Error($"Error stopping DeX: {ex.Message}");
+ await Task.Delay(800);
+ return false;
}
-
- await Task.Delay(800);
}
public async Task StartSingleWindowAsync()
@@ -599,8 +761,10 @@ public async Task ManageFileTransferAsync()
await Task.Delay(1000);
}
- public async Task RunDiagnosticsAsync()
+ public async Task RunDiagnosticsAsync(
+ CancellationToken cancellationToken = default)
{
+ await WaitForDeviceSnapshotAsync(cancellationToken);
AnsiConsole.Header("SYSTEM ENVIRONMENT & DIAGNOSTICS");
AnsiConsole.Info("Running environment checks...");
@@ -623,7 +787,7 @@ public async Task RunDiagnosticsAsync()
var diag = new DeviceVersionDiagnosticService(_adbService).Inspect(serial, device);
var companionStatus = _permissionService.Inspect(serial);
var report = _diagnosticReportService.CreateReport(
- "2.0.0",
+ Program.Version,
_adbService.AdbPath,
"Android Debug Bridge",
_settings.Paths.ScrcpyPath,
@@ -658,7 +822,19 @@ public async Task ManageCompanionAppAsync()
Console.WriteLine($" Detail: {status.Detail}");
Console.WriteLine($" Installed Package Code: {status.VersionCode}");
- Console.WriteLine("\n [1] Install Bundled Companion APK & Grant Permission");
+ var bundled = _permissionService.InspectBundledApk();
+ Console.WriteLine($" Bundled Companion APK: {bundled.State}");
+ if (bundled.State == BundledCompanionState.Ready)
+ {
+ Console.WriteLine("\n [1] Install Bundled Companion APK & Grant Permission");
+ }
+ else
+ {
+ Console.WriteLine("\n [1] Install Bundled Companion APK & Grant Permission (unavailable)");
+ AnsiConsole.Warning(
+ "This package does not contain a verified DX Companion APK. " +
+ "Automatic installation is unavailable.");
+ }
Console.WriteLine(" [2] Grant WRITE_SECURE_SETTINGS Permission Only");
Console.WriteLine(" [3] Return to Main Menu");
Console.Write("\nChoice: ");
@@ -666,6 +842,14 @@ public async Task ManageCompanionAppAsync()
var choice = Console.ReadLine()?.Trim();
if (choice == "1")
{
+ if (bundled.State != BundledCompanionState.Ready)
+ {
+ AnsiConsole.Error(
+ "Installation was not started because a verified bundled APK is unavailable.");
+ await Task.Delay(1000);
+ return;
+ }
+
AnsiConsole.Info("Installing Companion APK and granting permissions...");
var res = _permissionService.InstallAndGrant(serial);
if (res.State == DisplayCleanupPermissionState.Granted || res.State == DisplayCleanupPermissionState.Ready)
@@ -752,28 +936,133 @@ private void ViewRecentLogs()
Console.ReadLine();
}
- public void Shutdown()
+ private async Task WaitForDeviceSnapshotAsync(
+ CancellationToken cancellationToken = default)
+ {
+ _deviceMonitor.Start();
+ var pollTimeoutMs = (int)Math.Min(
+ 60000L,
+ Math.Max(
+ 15000L,
+ (long)_settings.Timing.ProcessTimeoutMs * 3L));
+ var completed = await Task.Run(
+ () => _deviceMonitor.WaitForFirstPoll(
+ pollTimeoutMs,
+ cancellationToken),
+ cancellationToken);
+ if (!completed)
+ {
+ AnsiConsole.Warning(
+ "The first device scan did not finish before the timeout. " +
+ "The connected-device list may still be incomplete.");
+ }
+ }
+
+ public async Task ShutdownAsync()
{
- if (!_isRunning) return;
+ if (Interlocked.Exchange(ref _shutdownStarted, 1) != 0) return;
+
_isRunning = false;
+ var device = GetSelectedDevice();
+ var fallbackSerial = GetPrimarySerial(device) ??
+ _selectedDeviceSerial;
+ var fallbackIdentity = device?.Identity ??
+ _activeRuntime?.Dex.CurrentSession?.DeviceIdentity ??
+ _selectedDeviceIdentity ??
+ string.Empty;
+ var errors = new List();
AnsiConsole.Info("Shutting down DX Manager and cleaning up active sessions...");
try
{
_deviceMonitor?.Stop();
- if (_activeRuntime != null)
+ }
+ catch (Exception ex)
+ {
+ errors.Add(ex);
+ }
+
+ try
+ {
+ _deviceMonitor?.Dispose();
+ }
+ catch (Exception ex)
+ {
+ errors.Add(ex);
+ }
+
+ try
+ {
+ _keyboardService?.Dispose();
+ }
+ catch (Exception ex)
+ {
+ errors.Add(ex);
+ }
+
+ if (_activeRuntime != null)
+ {
+ try
{
- _activeRuntime.Dex.RequestShutdown();
+ _activeRuntime.FileTransfers.RequestShutdown();
+ _activeRuntime.PhoneTransfers.RequestShutdown();
+ _activeRuntime.CompanionGuardian.RequestShutdown();
+ _activeRuntime.ScreenOff.RequestShutdown();
_activeRuntime.SingleWindows.RequestShutdown();
- _activeRuntime.Scrcpy.RequestShutdown();
+ _activeRuntime.Dex.RequestShutdown();
+ }
+ catch (Exception ex)
+ {
+ errors.Add(ex);
+ }
+
+ try
+ {
+ _activeRuntime.SingleWindows.StopAll();
+ }
+ catch (Exception ex)
+ {
+ errors.Add(ex);
+ }
+
+ try
+ {
+ await _activeRuntime.Dex.ShutdownAsync(
+ fallbackSerial,
+ fallbackIdentity);
+ if (_activeRuntime.Dex.HasDeferredDisplayCleanup)
+ {
+ errors.Add(new InvalidOperationException(
+ "DeX display cleanup was deferred because the " +
+ "target device was unavailable."));
+ }
+ }
+ catch (Exception ex)
+ {
+ errors.Add(ex);
}
}
- catch (Exception ex)
+
+ DisposeRuntimeServices(errors);
+
+ foreach (var error in errors)
{
- AnsiConsole.Error($"Error during shutdown: {ex.Message}");
+ AnsiConsole.Error($"Error during shutdown: {error.Message}");
}
- AnsiConsole.Success("DX Manager stopped cleanly. Goodbye!");
+ if (errors.Count == 0)
+ {
+ AnsiConsole.Success("DX Manager stopped cleanly. Goodbye!");
+ }
+ else
+ {
+ AnsiConsole.Warning("DX Manager stopped, but one or more cleanup steps could not be confirmed.");
+ }
+ }
+
+ public void Shutdown()
+ {
+ ShutdownAsync().GetAwaiter().GetResult();
}
public void Dispose()
@@ -781,7 +1070,35 @@ public void Dispose()
if (_disposed) return;
_disposed = true;
Shutdown();
- _deviceMonitor?.Dispose();
- _keyboardService?.Dispose();
+ }
+
+ private void DisposeRuntimeServices(ICollection errors)
+ {
+ if (Interlocked.Exchange(ref _runtimeServicesDisposed, 1) != 0)
+ return;
+ if (_activeRuntime == null) return;
+
+ var disposables = new IDisposable[]
+ {
+ _activeRuntime.SingleWindows,
+ _activeRuntime.Scrcpy,
+ _activeRuntime.ScreenOff,
+ _activeRuntime.PhoneTransfers,
+ _activeRuntime.CompanionGuardian,
+ _activeRuntime.FileTransfers
+ };
+
+ foreach (var disposable in disposables)
+ {
+ try
+ {
+ disposable.Dispose();
+ }
+ catch (Exception ex)
+ {
+ _logService.Error("macOS runtime service disposal failed.", ex);
+ errors.Add(ex);
+ }
+ }
}
}
diff --git a/DexManager.Mac/Platform/MacPathProvider.cs b/DexManager.Mac/Platform/MacPathProvider.cs
index 2b9e0c9..1483fd9 100644
--- a/DexManager.Mac/Platform/MacPathProvider.cs
+++ b/DexManager.Mac/Platform/MacPathProvider.cs
@@ -4,6 +4,21 @@ namespace DexManager.Mac.Platform;
public sealed class MacPathProvider : IPathProvider
{
+ private readonly bool _preferBundledTools;
+
+ public MacPathProvider()
+ : this(null)
+ {
+ }
+
+ internal MacPathProvider(bool? preferBundledTools)
+ {
+ _preferBundledTools = preferBundledTools ?? File.Exists(
+ Path.Combine(BaseDirectory, "PORTABLE_PACKAGE.txt"));
+ }
+
+ internal bool IsPortablePackage => _preferBundledTools;
+
public string BaseDirectory => AppDomain.CurrentDomain.BaseDirectory;
public string DefaultSettingsFilePath =>
@@ -70,23 +85,32 @@ public string ResolveDefaultScrcpyPath()
public string[] GetCandidateAdbPaths()
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ var bundledScrcpyAdb = Path.Combine(BaseDirectory, "tools", "scrcpy", "adb");
+ var bundledFallbackAdb = Path.Combine(BaseDirectory, "tools", "adb", "adb");
var candidates = new List
{
- Path.Combine(BaseDirectory, "tools", "adb", "adb"),
- Path.Combine(BaseDirectory, "tools", "scrcpy", "adb"),
- Path.Combine(home, "Downloads", "scrcpy-macos-x86_64-v3.3.4", "adb"),
- Path.Combine(home, "Downloads", "scrcpy-macos-aarch64-v3.3.4", "adb"),
Path.Combine(home, "Library", "Android", "sdk", "platform-tools", "adb"),
"/opt/homebrew/bin/adb",
"/usr/local/bin/adb",
Path.Combine(home, ".android-sdk", "platform-tools", "adb"),
"/opt/android-sdk/platform-tools/adb",
+ bundledScrcpyAdb,
+ bundledFallbackAdb,
Path.Combine(BaseDirectory, "tools", "adb", "adb.exe")
};
var inPath = FindInPath("adb");
- if (!string.IsNullOrWhiteSpace(inPath) && !candidates.Contains(inPath))
+ if (_preferBundledTools)
+ {
+ candidates.Remove(bundledScrcpyAdb);
+ candidates.Insert(0, bundledScrcpyAdb);
+ }
+ else if (!string.IsNullOrWhiteSpace(inPath))
{
+ candidates.RemoveAll(path => string.Equals(
+ path,
+ inPath,
+ StringComparison.Ordinal));
candidates.Insert(0, inPath);
}
@@ -95,21 +119,28 @@ public string[] GetCandidateAdbPaths()
public string[] GetCandidateScrcpyPaths()
{
- var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ var bundledScrcpy = Path.Combine(BaseDirectory, "tools", "scrcpy", "scrcpy");
var candidates = new List
{
- Path.Combine(BaseDirectory, "tools", "scrcpy", "scrcpy"),
- Path.Combine(home, "Downloads", "scrcpy-macos-x86_64-v3.3.4", "scrcpy"),
- Path.Combine(home, "Downloads", "scrcpy-macos-aarch64-v3.3.4", "scrcpy"),
"/opt/homebrew/bin/scrcpy",
"/usr/local/bin/scrcpy",
"/usr/bin/scrcpy",
+ bundledScrcpy,
Path.Combine(BaseDirectory, "tools", "scrcpy", "scrcpy.exe")
};
var inPath = FindInPath("scrcpy");
- if (!string.IsNullOrWhiteSpace(inPath) && !candidates.Contains(inPath))
+ if (_preferBundledTools)
+ {
+ candidates.Remove(bundledScrcpy);
+ candidates.Insert(0, bundledScrcpy);
+ }
+ else if (!string.IsNullOrWhiteSpace(inPath))
{
+ candidates.RemoveAll(path => string.Equals(
+ path,
+ inPath,
+ StringComparison.Ordinal));
candidates.Insert(0, inPath);
}
@@ -121,7 +152,7 @@ private static string FindInPath(string binaryName)
var pathEnv = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrWhiteSpace(pathEnv)) return null;
- foreach (var segment in pathEnv.Split([':'], StringSplitOptions.RemoveEmptyEntries))
+ foreach (var segment in pathEnv.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries))
{
try
{
diff --git a/DexManager.Mac/Platform/MacPlatformService.cs b/DexManager.Mac/Platform/MacPlatformService.cs
index 54c9e15..fefb637 100644
--- a/DexManager.Mac/Platform/MacPlatformService.cs
+++ b/DexManager.Mac/Platform/MacPlatformService.cs
@@ -59,7 +59,7 @@ public bool IsDirectoryInProcessPath(string directory)
if (string.IsNullOrWhiteSpace(directory)) return false;
var norm = Path.GetFullPath(directory).TrimEnd('/');
var path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
- foreach (var part in path.Split([':'], StringSplitOptions.RemoveEmptyEntries))
+ foreach (var part in path.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries))
{
if (string.Equals(Path.GetFullPath(part).TrimEnd('/'), norm, StringComparison.Ordinal))
return true;
diff --git a/DexManager.Mac/Program.cs b/DexManager.Mac/Program.cs
index 801df35..472c4b3 100644
--- a/DexManager.Mac/Program.cs
+++ b/DexManager.Mac/Program.cs
@@ -1,3 +1,4 @@
+using System.Reflection;
using System.Text;
using DexManager.Mac.Hosting;
@@ -5,7 +6,10 @@ namespace DexManager.Mac;
internal static class Program
{
- private const string Version = "2.0.0 (macOS Edition)";
+ internal static string Version =>
+ Assembly.GetExecutingAssembly()
+ .GetCustomAttribute()?
+ .InformationalVersion ?? "unknown";
private static async Task Main(string[] args)
{
@@ -43,15 +47,19 @@ private static async Task Main(string[] args)
switch (cmd)
{
case "--diag" or "-d":
- await host.RunDiagnosticsAsync();
+ await host.RunDiagnosticsAsync(cts.Token);
return 0;
case "--dex" or "-x":
- await host.StartDexAsync();
+ if (!await host.StartDexAsync(cts.Token))
+ {
+ return 1;
+ }
AnsiConsole.Info("DeX launched. Press Ctrl+C to stop...");
try
{
- while (!cts.Token.IsCancellationRequested)
+ while (!cts.Token.IsCancellationRequested &&
+ host.IsDexRunning)
{
await Task.Delay(500, cts.Token);
}
@@ -60,12 +68,32 @@ private static async Task Main(string[] args)
{
// Expected on Ctrl+C
}
- await host.StopDexAsync();
- return 0;
+ if (!cts.Token.IsCancellationRequested)
+ {
+ try
+ {
+ if (await host.WaitForDexCleanupAsync(cts.Token))
+ {
+ return 0;
+ }
+ if (!cts.Token.IsCancellationRequested)
+ {
+ return 1;
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Continue through the atomic stop/cleanup path.
+ }
+ }
+ return await host.StopDexAsync() ? 0 : 1;
case "--stop-dex":
- await host.StopDexAsync();
- return 0;
+ return await host.StopDexAsync(
+ cleanupUntrackedOverlay: true,
+ cancellationToken: cts.Token)
+ ? 0
+ : 1;
default:
AnsiConsole.Warning($"Unknown option: '{args[0]}'. Showing help:");
@@ -92,12 +120,14 @@ private static void PrintHelp()
{
AnsiConsole.Header($"DX MANAGER for macOS (.NET 8) - v{Version}");
Console.WriteLine("Usage:");
- Console.WriteLine(" dotnet run --project DexManager.Mac # Start Interactive Dashboard");
- Console.WriteLine(" dotnet run --project DexManager.Mac -- --dex # Launch DeX immediately (Ctrl+C to stop)");
- Console.WriteLine(" dotnet run --project DexManager.Mac -- --stop-dex # Stop active DeX session");
- Console.WriteLine(" dotnet run --project DexManager.Mac -- --diag # Run system diagnostics");
- Console.WriteLine(" dotnet run --project DexManager.Mac -- --version # Display version info");
- Console.WriteLine(" dotnet run --project DexManager.Mac -- --help # Show this help message");
+ Console.WriteLine(" DXManager.Mac # Start Interactive Dashboard");
+ Console.WriteLine(" DXManager.Mac --dex # Launch DeX immediately (Ctrl+C to stop)");
+ Console.WriteLine(" DXManager.Mac --stop-dex # Stop active DeX session");
+ Console.WriteLine(" DXManager.Mac --diag # Run system diagnostics");
+ Console.WriteLine(" DXManager.Mac --version # Display version info");
+ Console.WriteLine(" DXManager.Mac --help # Show this help message");
+ Console.WriteLine();
+ Console.WriteLine("Source developers may use: dotnet run --project DexManager.Mac -- [option]");
Console.WriteLine();
}
}
diff --git a/DexManager.Tests/MacPathProviderTests.cs b/DexManager.Tests/MacPathProviderTests.cs
index 05f1618..97dda5d 100644
--- a/DexManager.Tests/MacPathProviderTests.cs
+++ b/DexManager.Tests/MacPathProviderTests.cs
@@ -74,6 +74,19 @@ public void GetCandidateAdbPaths_ContainsHomebrewAndAndroidSdkLocations()
Assert.Contains(candidates, p => p.Contains("/opt/android-sdk/platform-tools/adb"));
}
+ [Fact]
+ public void GetCandidateAdbPaths_PrefersBundledPortableAdb()
+ {
+ var provider = new MacPathProvider(preferBundledTools: true);
+ var candidates = provider.GetCandidateAdbPaths();
+
+ Assert.Equal(
+ Path.Combine(provider.BaseDirectory, "tools", "scrcpy", "adb"),
+ candidates[0]);
+ Assert.DoesNotContain(candidates, p =>
+ p.Contains(Path.Combine("Downloads", "scrcpy-macos")));
+ }
+
[Fact]
public void GetCandidateScrcpyPaths_ContainsHomebrewAndLocalLocations()
{
@@ -86,6 +99,32 @@ public void GetCandidateScrcpyPaths_ContainsHomebrewAndLocalLocations()
Assert.Contains(candidates, p => p.Contains("/usr/bin/scrcpy"));
}
+ [Fact]
+ public void GetCandidateScrcpyPaths_PrefersBundledPortableScrcpy()
+ {
+ var provider = new MacPathProvider(preferBundledTools: true);
+ var candidates = provider.GetCandidateScrcpyPaths();
+
+ Assert.Equal(
+ Path.Combine(provider.BaseDirectory, "tools", "scrcpy", "scrcpy"),
+ candidates[0]);
+ Assert.DoesNotContain(candidates, p =>
+ p.Contains(Path.Combine("Downloads", "scrcpy-macos")));
+ }
+
+ [Fact]
+ public void DevelopmentBuild_DoesNotForceBundledToolsAheadOfNativeTools()
+ {
+ var provider = new MacPathProvider(preferBundledTools: false);
+
+ Assert.NotEqual(
+ Path.Combine(provider.BaseDirectory, "tools", "scrcpy", "scrcpy"),
+ provider.GetCandidateScrcpyPaths()[0]);
+ Assert.NotEqual(
+ Path.Combine(provider.BaseDirectory, "tools", "scrcpy", "adb"),
+ provider.GetCandidateAdbPaths()[0]);
+ }
+
[Fact]
public void ResolveDefaultAdbPath_ReturnsValidString()
{
diff --git a/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md b/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md
new file mode 100644
index 0000000..f96cd07
--- /dev/null
+++ b/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md
@@ -0,0 +1,66 @@
+# DX Manager for macOS - Third-Party Notices
+
+DX Manager is an independently developed utility. The macOS portable packages
+include the components listed below from official project sources or archives.
+Packaging applies ad-hoc macOS signatures to the DX Manager executables and to
+a third-party executable only when it does not already have a valid signature.
+This changes signing metadata, not program code. Those components remain
+under their own licenses; the DX Manager MIT License does not replace or alter
+those licenses.
+
+## .NET Runtime
+
+The self-contained DX Manager and ADB proxy executables include Microsoft .NET
+Runtime 8.0.30 components. .NET Runtime is licensed under the MIT License.
+The package includes `dotnet-LICENSE.txt` and the matching
+`dotnet-THIRD-PARTY-NOTICES.txt` from the official `dotnet/runtime` v8.0.30
+source tag.
+
+Official source: https://github.com/dotnet/runtime/tree/v8.0.30
+
+## scrcpy
+
+The package includes the official static scrcpy 4.1 archive for the selected
+Mac processor architecture. scrcpy and scrcpy-server are licensed under the
+Apache License, Version 2.0. The complete license supplied by the official
+archive is retained in `tools/scrcpy/LICENSE` and is also provided as
+`scrcpy-LICENSE.txt` in the package license directory.
+
+Copyright (C) 2018 Genymobile
+
+Copyright (C) 2018-2026 Romain Vimont
+
+Official project: https://github.com/Genymobile/scrcpy
+
+## Android Debug Bridge (ADB)
+
+The official scrcpy macOS archive includes an Android Debug Bridge binary from
+Android SDK Platform-Tools. ADB source is distributed under the Apache License,
+Version 2.0.
+
+Official source: https://android.googlesource.com/platform/packages/modules/adb/
+
+Platform-Tools information: https://developer.android.com/tools/releases/platform-tools
+
+## Libraries linked into the static scrcpy client
+
+The official scrcpy 4.1 macOS build links libraries including SDL 3.4.12,
+FFmpeg 8.1.2, libusb 1.0.30, dav1d 1.5.3, and zlib. The package provides the
+applicable LGPL 2.1, SDL/zlib, dav1d BSD 2-Clause, and zlib license texts in its
+`licenses` directory. Source and build information is available from:
+
+- https://github.com/Genymobile/scrcpy/blob/v4.1/release/build_macos.sh
+- https://ffmpeg.org/
+- https://github.com/libsdl-org/SDL/tree/release-3.4.12
+- https://github.com/libusb/libusb/tree/v1.0.30
+- https://code.videolan.org/videolan/dav1d
+- https://zlib.net/
+
+## Samsung DeX trademark notice
+
+DX Manager works with features provided by Samsung DeX on compatible Samsung
+devices. DX Manager is not affiliated with, sponsored by, endorsed by, or
+distributed by Samsung Electronics or Genymobile.
+
+Samsung and Samsung DeX are trademarks of Samsung Electronics Co., Ltd. All
+trademarks are the property of their respective owners.
diff --git a/README.md b/README.md
index 83857e1..8b80a47 100644
--- a/README.md
+++ b/README.md
@@ -86,12 +86,15 @@ distribution.
- Light, dark, and Windows-following themes
- Automatic Korean/English UI selection
- Session logs and environment diagnostics
-- Optional bundled DX Companion with verified install, update, removal, and
+- Optional DX Companion in the Windows package, with verified install, update, removal, and
one-time permission grant; it provides virtual-display and Stay-awake
recovery, phone-to-PC file transfer, a Quick Settings tile, and a compact
home-screen widget
- Dual OS support: Dedicated Windows WinForms GUI and native macOS (.NET 8 TUI & CLI host)
-- 64-bit Windows 7 SP1 to Windows 11 compatibility (.NET 4.6.2) and macOS 12+ (Apple Silicon & Intel x86_64)
+- 64-bit Windows 7 SP1 to Windows 11 compatibility (.NET 4.6.2), plus macOS 14+
+ portable targets for Apple Silicon and Intel x86_64. The Intel package has
+ passed Rosetta executable checks; a physical Intel Mac DeX session is not yet
+ verified.
## Design Philosophy
@@ -102,7 +105,9 @@ real Samsung DeX use. The project prioritizes:
- Automation over repetitive manual work
- Practical usability over unnecessary complexity
-## Requirements
+## Windows Requirements
+
+For the prebuilt macOS package, see the [macOS portable package guide](docs/PACKAGE_README_MACOS.md).
- 64-bit Windows 7 SP1, 8.1, 10, or 11 (32-bit Windows is not supported)
- .NET Framework 4.6.2 or later
@@ -133,7 +138,7 @@ Wireless ADB additionally requires the PC and phone to communicate on the
same local network. Guest Wi-Fi, AP isolation, VLAN rules, or corporate
network policies may block the connection.
-## Quick Start
+## Windows Quick Start
1. Download the release ZIP and extract the entire folder to a user-writable
location. Avoid protected folders such as `Program Files` unless write
@@ -259,8 +264,9 @@ and an informational compatibility assessment. **Save diagnostic report**
creates a privacy-redacted text report containing the environment, selected
device state, connected-device summary, and recent warnings/errors.
-The signed **DX Companion 2.0.0** APK is included as an external file under
-`tools\companion`, but it is never installed automatically. In **Settings >
+In the Windows release ZIP, the signed **DX Companion 2.0.0** APK is included
+as an external file under `tools\companion`, but it is never installed
+automatically. In **Settings >
Diagnostics > DX Companion**, the user can install, update, reinstall, grant
the required permission, or uninstall it on the currently selected phone.
@@ -315,24 +321,40 @@ scripts/Package-Release.ps1
See [DexManager/README.md](DexManager/README.md) for packaging notes.
### 2. macOS Edition (Cross-Platform Host & TUI Dashboard)
-- **Target**: macOS 12+ (Apple Silicon & Intel x86_64)
-- **Toolchain**: .NET 8.0 SDK (`net8.0`), scrcpy 3.3.4+ / 4.x
+- **Target**: macOS 14 Sonoma or later (Apple Silicon & Intel x86_64)
+- **Portable packages**: `DX-Manager-v2.0.0-macos-arm64.zip` and
+ `DX-Manager-v2.0.0-macos-x64.zip`
+- **End-user prerequisites**: no Homebrew and no separate .NET installation;
+ the packages include a self-contained .NET runtime, scrcpy 4.1, ADB, and the
+ scrcpy server
+- **Developer toolchain**: pinned .NET 8 SDK (`global.json`)
- **Solution**: `DexManager.Mac.sln`
-- **Output**: `DexManager.Mac/bin/Release/net8.0`
+- **Developer packaging output**: `dist/DX-Manager-v2.0.0-macos-*.zip`
- **Features**: Interactive terminal UI (TUI) dashboard, CLI argument mode (`--dex`, `--diag`), native macOS path resolution, and multi-device support.
-```bash
-# Build on macOS
-export PATH="$HOME/.dotnet:$PATH"
-dotnet build DexManager.Mac.sln -c Release
+Users download the package matching their Mac, extract the complete ZIP, and
+double-click `Start DX Manager.command`. They do not build the source. A
+version tag such as `v2.0.0` is configured to trigger fresh Apple Silicon and
+Intel builds. When both jobs succeed, the workflow creates a draft GitHub
+Release containing both verified ZIPs and their SHA-256 files. A maintainer
+reviews the draft before publishing it. The first remote workflow run for this
+change still needs to be confirmed.
-# Run Interactive Dashboard
-dotnet run --project DexManager.Mac -c Release
+The following commands are for maintainers who need to reproduce the packages
+locally:
+
+```bash
+# Build and verify a prebuilt Apple Silicon portable ZIP
+scripts/Package-Mac-Release.sh --rid osx-arm64
-# Run all unit and integration tests
-dotnet test DexManager.Mac.sln -c Release
+# Build and verify a prebuilt Intel portable ZIP
+scripts/Package-Mac-Release.sh --rid osx-x64
```
-See the detailed [macOS Guide (docs/MACOS_GUIDE.md)](docs/MACOS_GUIDE.md) for setup and usage.
+The repository workflow is configured to run the build and test suites on
+matching Apple Silicon and Intel runners. Pull requests retain both packages as
+workflow artifacts; version tags create a draft Release with the same verified
+files. See the [portable package guide](docs/PACKAGE_README_MACOS.md)
+and the detailed [macOS guide](docs/MACOS_GUIDE.md).
## Project Status
@@ -449,10 +471,12 @@ DX Manager는 개인적으로 사용하던 Batch 스크립트, CMD 명령과 Aut
- 설정 시간 동안 미입력 시 시스템 트레이 자동 숨김
- 라이트, 다크 및 Windows 설정 연동 테마
- Windows 언어에 따른 한국어·영어 UI 자동 선택
-- 가상화면과 절전모드 해제 복구, 휴대폰→PC 파일 전송, 빠른 설정 타일과
- 2 × 1 홈 위젯을 제공하는 공식 검증형 DX Companion 번들
+- Windows 패키지에는 가상화면과 절전모드 해제 복구, 휴대폰→PC 파일 전송,
+ 빠른 설정 타일과 2 × 1 홈 위젯을 제공하는 공식 검증형 DX Companion 번들
- Windows 및 macOS 2가지 플랫폼 전용 빌드 지원 (Windows WinForms GUI & macOS .NET 8 TUI 대시보드/CLI)
-- 64비트 Windows 7 SP1~11 (.NET 4.6.2) 및 macOS 12 Monterey 이상 (Apple Silicon / Intel) 지원
+- 64비트 Windows 7 SP1~11 (.NET 4.6.2) 호환 및 macOS 14 Sonoma 이상
+ Apple Silicon/Intel용 포터블 패키지 제공 목표. Intel 패키지는 Rosetta 기동을
+ 확인했으며 실제 Intel Mac의 DeX 실기는 아직 확인하지 않았습니다.
## 개발 철학
@@ -463,7 +487,9 @@ DX Manager의 모든 기능은 Samsung DeX를 실제로 사용하면서 겪은
- 반복적인 수동 작업보다 자동화
- 불필요한 복잡함보다 실용성
-## 요구 사항
+## Windows 요구 사항
+
+미리 빌드된 macOS 패키지는 [macOS 포터블 패키지 안내](docs/PACKAGE_README_MACOS.md)를 참조하십시오.
- 64비트 Windows 7 SP1, 8.1, 10 또는 11(32비트 Windows는 지원하지 않음)
- .NET Framework 4.6.2 이상
@@ -494,7 +520,7 @@ DX Manager 빌드가 실행됩니다.
수 있어야 합니다. 게스트 Wi-Fi, AP 격리, VLAN 규칙 또는 회사 네트워크
정책으로 연결이 차단될 수 있습니다.
-## 빠른 시작
+## Windows 빠른 시작
1. 릴리스 ZIP을 내려받아 현재 계정이 쓸 수 있는 위치에 폴더 전체의 압축을
풉니다. 별도 쓰기 권한을 설정하지 않았다면 `Program Files` 같은 보호
@@ -614,8 +640,9 @@ Windows 환경에서 비ASCII 파일명을 보존하지 못할 수 있습니다.
환경·선택 기기 상태·연결 기기 요약·최근 경고와 오류를 개인정보를 가린 텍스트
파일로 저장합니다.
-서명된 **DX Companion 2.0.0** APK는 공개 ZIP의 `tools\companion`에 외부 파일
-형태로 포함되지만 자동으로 설치되지 않습니다. **설정 > 진단 > DX Companion**에서
+Windows 공개 ZIP에는 서명된 **DX Companion 2.0.0** APK가
+`tools\companion`에 외부 파일 형태로 포함되지만 자동으로 설치되지 않습니다.
+**설정 > 진단 > DX Companion**에서
사용자가 현재 선택된 휴대폰에 설치·업데이트·재설치하거나 권한을 부여하고 삭제할
수 있습니다.
@@ -669,24 +696,37 @@ scripts/Package-Release.ps1
배포 파일 구성은 [DexManager/README.md](DexManager/README.md)를 참조하십시오.
### 2. macOS 에디션 (크로스플랫폼 호스트 & TUI 대시보드)
-- **지원 환경**: macOS 12 Monterey 이상 (Apple Silicon 및 Intel x86_64)
-- **개발 환경**: .NET 8.0 SDK (`net8.0`), scrcpy 3.3.4 이상 / 4.x
+- **지원 환경**: macOS 14 Sonoma 이상 (Apple Silicon 및 Intel x86_64)
+- **포터블 패키지**: `DX-Manager-v2.0.0-macos-arm64.zip` 및
+ `DX-Manager-v2.0.0-macos-x64.zip`
+- **사용자 사전 설치**: Homebrew와 별도 .NET 설치 불필요. self-contained
+ .NET 런타임, scrcpy 4.1, ADB와 scrcpy 서버를 ZIP에 포함
+- **개발 환경**: `global.json`에 고정된 .NET 8 SDK
- **솔루션**: `DexManager.Mac.sln`
-- **산출물**: `DexManager.Mac/bin/Release/net8.0`
+- **개발자용 패키징 산출물**: `dist/DX-Manager-v2.0.0-macos-*.zip`
- **특징**: ANSI 대화형 콘솔 대시보드(TUI), CLI 인자 실행 모드(`--dex`, `--diag`), macOS 표준 경로 및 다중 기기 독립 세션 제어.
-```bash
-# macOS 빌드
-export PATH="$HOME/.dotnet:$PATH"
-dotnet build DexManager.Mac.sln -c Release
+일반 사용자는 Mac에 맞는 패키지를 내려받아 ZIP 전체의 압축을 풀고
+`Start DX Manager.command`를 더블클릭합니다. 소스 빌드는 필요하지 않습니다.
+`v2.0.0` 같은 버전 태그를 push하면 Apple Silicon용과 Intel용 패키지를 새로
+빌드하도록 구성했습니다. 두 작업이 성공하면 검증된 ZIP 두 개와 SHA-256 파일이
+포함된 GitHub Release 초안을 만들며, 유지관리자가 확인한 뒤 공개합니다. 이 변경의
+첫 원격 workflow 성공 여부는 아직 확인해야 합니다.
-# 대화형 대시보드 실행
-dotnet run --project DexManager.Mac -c Release
+다음 명령은 패키지를 로컬에서 재현하려는 유지관리자용입니다.
+
+```bash
+# Apple Silicon 포터블 ZIP 미리 빌드 및 검증
+scripts/Package-Mac-Release.sh --rid osx-arm64
-# 전체 단위/통합 테스트 실행 (92개 테스트)
-dotnet test DexManager.Mac.sln -c Release
+# Intel 포터블 ZIP 미리 빌드 및 검증
+scripts/Package-Mac-Release.sh --rid osx-x64
```
-자세한 설치 및 조작 방법은 [macOS 가이드 (docs/MACOS_GUIDE.md)](docs/MACOS_GUIDE.md)를 참조하십시오.
+저장소의 GitHub Actions workflow는 Apple Silicon과 Intel 실행 환경에서 각각
+빌드·테스트하도록 구성되어 있습니다. PR에서는 두 패키지를 workflow artifact로
+보관하고, 버전 태그에서는 같은 검증 파일을 넣은 Release 초안을 만듭니다.
+[포터블 패키지 안내](docs/PACKAGE_README_MACOS.md)와 [macOS
+가이드](docs/MACOS_GUIDE.md)를 참조하십시오.
## 프로젝트 상태
diff --git a/build/macos/PORTABLE_PACKAGE.txt b/build/macos/PORTABLE_PACKAGE.txt
new file mode 100644
index 0000000..eb27c89
--- /dev/null
+++ b/build/macos/PORTABLE_PACKAGE.txt
@@ -0,0 +1,2 @@
+This file marks a complete DX Manager portable package.
+Keep it beside DXManager.Mac so the bundled scrcpy and ADB are selected first.
diff --git a/build/macos/Start DX Manager.command b/build/macos/Start DX Manager.command
new file mode 100755
index 0000000..e2aebfe
--- /dev/null
+++ b/build/macos/Start DX Manager.command
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+set -u
+
+launcher_directory="$(cd "$(dirname "$0")" && pwd)"
+application="$launcher_directory/DXManager.Mac"
+
+cd "$launcher_directory" || exit 1
+export PATH="$launcher_directory/tools/scrcpy:/usr/bin:/bin:/usr/sbin:/sbin"
+
+if [[ ! -x "$application" ]]; then
+ echo "DX Manager could not start because its executable is missing or is not executable."
+ echo "Extract the complete ZIP again and keep every file in the same folder."
+ read -r -p "Press Return to close this window. "
+ exit 1
+fi
+
+"$application" "$@"
+exit_code=$?
+
+if [[ $exit_code -ne 0 ]]; then
+ echo
+ echo "DX Manager exited with status $exit_code."
+ echo "Review the message above, then press Return to close this window."
+ read -r
+fi
+
+exit "$exit_code"
diff --git a/docs/AI_WORKFLOW.md b/docs/AI_WORKFLOW.md
index 0098134..7715455 100644
--- a/docs/AI_WORKFLOW.md
+++ b/docs/AI_WORKFLOW.md
@@ -48,6 +48,24 @@
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\Package-Release.ps1
```
+macOS 공개 후보는 사용자가 빌드하지 않아도 되도록 아키텍처별 self-contained
+ZIP을 미리 만든다.
+
+```bash
+scripts/Package-Mac-Release.sh --rid osx-arm64
+scripts/Package-Mac-Release.sh --rid osx-x64
+```
+
+macOS 스크립트는 현재 저장소의 x86 전용 개발 도구를 그대로 복사하지 않는다.
+DX Manager와 `DXMAdbProxy`를 대상 RID로 publish하고, SHA-256을 고정한 공식
+scrcpy 4.1 macOS 정적 아카이브를 아키텍처별로 넣는다. ZIP을 다시 풀어 실행
+권한, Mach-O 아키텍처, 외부 Homebrew·사용자 경로 의존성, 버전, 필수 문서와
+사용자 데이터·디버그 파일 제외를 확인한다. 파일 권한·순서·시각은
+`SOURCE_DATE_EPOCH` 또는 현재 Git 커밋 시각으로 정규화한다. GitHub Actions
+workflow는 `macos-15`와 `macos-15-intel`에서 두 패키지를 각각 빌드·테스트해
+PR artifact로 보관한다. `v*` 버전 태그에서는 두 ZIP과 체크섬을 다시 검증한 뒤
+Release 초안을 만들고, 유지관리자가 초안을 확인한 뒤 공개한다.
+
시스템에 .NET Framework 4.6.2 Developer Pack이 없으면 참조 어셈블리 루트를
`-TargetFrameworkRootPath`로 지정하거나 현재 셸의
`DXM_TARGET_FRAMEWORK_ROOT` 환경 변수에 설정한다.
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index e16c383..8c60374 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -4,13 +4,30 @@
## 2026-08 - v2.0.0 (macOS Cross-Platform Edition)
+- Apple Silicon arm64와 Intel x64용 self-contained 포터블 ZIP 패키징 추가
+- GitHub Actions가 각 Mac 아키텍처에서 단위·통합 테스트와 다중 기기 테스트를
+ 실행한 뒤 미리 빌드된 ZIP과 SHA-256 파일을 artifact로 업로드하고, 버전
+ 태그에서는 정확한 파일명과 체크섬을 확인해 Release 초안을 만들도록 구성
+- 공식 scrcpy 4.1 macOS 정적 아카이브의 SHA-256, Mach-O 아키텍처, 실행 권한,
+ 외부 Homebrew 경로 의존성과 배포 제외 파일을 자동 검증
+- `Start DX Manager.command` 더블클릭 실행기와 macOS 패키지 전용 영어·한국어
+ README 및 .NET Runtime 제3자 고지 추가
+- `Q` 종료가 실행 중인 DeX·단일창·전송 서비스를 실제로 중지하고 연결된 기기의
+ overlay 정리를 기다리도록 종료 경로 수정
+- DeX 시작과 overlay 제거 직전에 물리 기기 identity를 다시 조회하고, 보류된
+ 정리를 identity별 독립 항목으로 보존해 같은 Wi-Fi endpoint를 다른 휴대폰이
+ 재사용해도 이전 기기의 정리 기록을 덮어쓰거나 잘못 제거하지 않도록 보강
+- CLI 시작·자연 종료 대기 중 `Ctrl+C`를 실제 시작 취소와 원자적 종료 확인 경로에
+ 연결하고 Scrcpy 종료 이벤트마다 누락 없는 overlay 정리 작업을 예약
+- USB로 시작한 세션에서 USB가 사라져도 같은 물리 identity로 검증된 Wi-Fi ADB
+ transport가 남아 있으면 해당 경로로 overlay를 정리하도록 전환 복구 추가
- **macOS 크로스플랫폼 포팅 완료 (.NET 8 Native)**:
- 플랫폼 독립적인 비즈니스 로직, 세션 레지스트리, ADB/Scrcpy 오케스트레이션을 `DexManager.Core` (.NET 8)로 분리
- macOS 네이티브 인터랙티브 호스트 및 TUI 대시보드 `DexManager.Mac` 구현 (ANSI 컬러 콘솔, 실시간 기기 감시, 키보드 인터랙션)
- macOS 플랫폼 서비스 구현 (`MacPlatformService`, `MacPathProvider`, `MacCaptureService` [screencapture 연동], `MacAutoStartService` [LaunchAgents plist])
- Scrcpy 파일 드롭 가로채기 및 관리형 전송 중계를 위한 `DexManager.AdbProxy` (.NET 8) 크로스플랫폼 이식
- C# 12 최신 문법(파일 범위 네임스페이스, 패턴 매칭, 레코드, 식 본문 멤버, 컬렉션 식) 적용 및 코드베이스 리팩터링
- - 92개 xUnit 단위/통합 테스트 (`DexManager.Tests`) 및 39개 다중 기기 독립 회귀 테스트 (`DexManager.MultiDeviceTests`) 100% 통과
+ - 95개 xUnit 단위/통합 테스트 (`DexManager.Tests`) 및 39개 다중 기기 독립 회귀 테스트 (`DexManager.MultiDeviceTests`) 100% 통과
- macOS 사용자 및 개발자 가이드 문서(`docs/MACOS_GUIDE.md`) 작성
## 2026-08 - v2.0.0
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
index a0e7358..1be9145 100644
--- a/docs/DECISIONS.md
+++ b/docs/DECISIONS.md
@@ -326,3 +326,45 @@ Scrcpy, DeX orchestrator, 단일창, 화면 OFF, Companion 수신기와 PC→휴
않는다. 코드 양보다 종료 순서, 슬롯별 상태, 사용자 창 닫기와 연결 분리 경합을
보존하는 것이 우선이다. 공통화는 이 경로를 직접 검증하는 테스트가 생긴 뒤 별도
작업으로 수행한다.
+
+## 2026-08 - macOS는 아키텍처별 self-contained ZIP으로 배포한다
+
+macOS 사용자가 저장소를 clone하거나 .NET, Homebrew, scrcpy와 ADB를 직접
+설치하도록 하지 않는다. 원본 Windows 프로젝트가 실행 파일과 도구 폴더 전체를
+미리 만든 ZIP으로 배포하는 것과 같은 원칙을 적용한다. Apple Silicon arm64와
+Intel x64는 하나의 universal ZIP으로 합치지 않고 각각 해당 RID로 DX Manager와
+ADB proxy를 self-contained publish한다.
+
+.NET single-file 내부 압축은 사용하지 않는다. macOS arm64 self-contained
+실행 파일을 같은 추출 캐시에서 연속 실행할 때 내부 압축을 켠 빌드에서
+`System.AccessViolationException`을 반복 재현했고, 압축만 끈 같은 빌드는
+연속 실행을 통과했다. 실행 파일은 계속 single-file로 publish하며 최종 ZIP의
+표준 압축은 유지한다.
+
+저장소의 개발용 `tools`에는 x86 전용 파일이 있을 수 있으므로 공개 패키징에서
+그 폴더를 무조건 복사하지 않는다. 공식 scrcpy 4.1 macOS 정적 아카이브를
+아키텍처별 URL과 SHA-256으로 고정해 넣고, 번들 도구를 PATH와 Homebrew보다 먼저
+사용한다. GitHub Actions도 실제 아키텍처가 일치하는 `macos-15`와
+`macos-15-intel`에서 별도로 빌드·실행 검증한다.
+
+DeX overlay를 만들거나 제거하기 직전에는 선택 화면에 남은 identity만 신뢰하지
+않고 해당 ADB transport에서 안정적인 물리 identity를 다시 읽는다. 이미 알고 있던
+안정 identity와 다르면 같은 Wi-Fi endpoint를 다른 휴대폰이 재사용한 것으로 보고
+작업을 중단한다. 처음 관찰한 identity가 임시 `transport:*` 값이면 live 조회로
+안정 identity를 얻은 경우에만 세션을 시작한다.
+
+세션을 시작한 transport가 사라졌더라도 같은 안정 identity의 다른 authorized
+transport가 연결돼 있으면 그 현재 serial에서 overlay reset을 수행한다. 예를 들어
+USB로 시작한 뒤 USB가 끊기고 같은 휴대폰의 Wi-Fi ADB가 남은 경우에는 Wi-Fi
+transport의 live identity를 다시 확인한 뒤 정리한다. 같은 endpoint의 다른
+identity나 identity를 확인할 수 없는 연결에는 정리 명령을 보내지 않는다.
+
+연결이 끊겨 overlay를 제거하지 못한 기록은 serial 하나를 키로 덮어쓰지 않고
+물리 identity와 lease를 함께 가진 독립 항목으로 보존한다. 같은 identity의 새
+transport에서 `EnsureVirtualDisplay`가 성공하거나 명시적 reset이 성공한 뒤에만
+해당 보류 기록을 완료 처리한다. 시작 전, identity 미확인 상태 또는 다른 기기에
+명령이 전달된 상태에서는 보류 기록을 삭제하지 않는다.
+
+Apple Developer ID와 notarization 자격 증명은 저장소에 넣지 않는다. 인증서가
+구성되기 전 자동 artifact에는 Developer ID 서명·공증이 없음을 문서에 표시하고
+Gatekeeper를 자동으로 우회하지 않는다.
diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md
index 12d133f..e84448d 100644
--- a/docs/KNOWN_ISSUES.md
+++ b/docs/KNOWN_ISSUES.md
@@ -140,3 +140,17 @@ DX Manager가 만든 화면과 사용자가 개발자 옵션에서 직접 선택
- README, 사용 설명서와 FAQ의 한국어/영어 스크린샷 배치 완료
- 공개 ZIP에서 개인 설정, PDB, 로그와 테스트 스크린샷 제외 확인
- 자동화 테스트 없이 주요 흐름은 실기 테스트에 의존
+
+## macOS 포터블 서명과 실기 범위
+
+macOS arm64/x64 ZIP은 self-contained이므로 Homebrew와 별도 .NET 설치에
+의존하지 않는다. 다만 현재 자동화 설정에는 Apple Developer ID 인증서와
+notarization 자격 증명이 구성되어 있지 않다. 브라우저로 받은 ZIP은 최초 실행
+시 Gatekeeper 승인이 필요할 수 있으며, 서명·공증 전에는 모든 macOS
+버전에서 경고 없는 최초 실행을 보장하지 않는다. 패키징 스크립트가 quarantine
+속성을 자동 삭제하거나 macOS 보안 기능을 우회하지 않는다.
+
+Apple Silicon ZIP은 Apple Silicon Mac에서 실제 바이너리 기동을 확인했다.
+Intel ZIP은 아키텍처와 외부 경로를 검사하고 Apple Silicon Mac의 Rosetta에서
+실행 파일 기동을 확인했지만, Intel 실기 전체 DeX 흐름은 GitHub의
+`macos-15-intel` 자동 빌드와 별도로 실제 Intel Mac 확인이 남아 있다.
diff --git a/docs/MACOS_GUIDE.md b/docs/MACOS_GUIDE.md
index d87f4c1..c86266b 100644
--- a/docs/MACOS_GUIDE.md
+++ b/docs/MACOS_GUIDE.md
@@ -16,53 +16,45 @@
## 2. 시스템 요구사항 (Requirements)
-- **운영체제**: macOS 12 Monterey 이상 (macOS 13 Ventura, macOS 14 Sonoma, macOS 15 Sequoia 완벽 지원)
+- **운영체제**: macOS 14 Sonoma 이상
- **아키텍처**:
- - Apple Silicon (M1 / M2 / M3 / M4) - Native ARM64
+ - Apple Silicon (Apple M series) - Native ARM64
- Intel Mac (x86_64)
-- **런타임 / SDK**: .NET 8.0 SDK 또는 .NET 8.0 Runtime
-- **필수 외부 도구**:
- - **Scrcpy**: 4.0 이상 (권장: Scrcpy 4.1+)
- - **Android Platform-Tools (ADB)**: 최신 버전
+- **포터블 ZIP 사용자**: Homebrew, .NET, scrcpy와 ADB를 별도로 설치하지
+ 않습니다. Mac 아키텍처에 맞게 미리 빌드된 ZIP에 self-contained .NET
+ 런타임, scrcpy 4.1, ADB와 scrcpy 서버가 포함됩니다.
+- **소스 개발자**: `global.json`에 지정된 .NET 8 SDK가 필요합니다.
- **지원 스마트폰**:
- Samsung Galaxy 기기 중 Samsung DeX를 지원하는 기기 (Galaxy S시리즈, Note시리즈, Z Fold시리즈, Tab S시리즈 등)
- Android 16 / One UI 8.x (현재 기준 동작 검증)
---
-## 3. 사전 환경 준비 (Prerequisites)
+## 3. 포터블 ZIP 실행 (Portable Release)
-### 3.1 Homebrew를 통한 필수 도구 설치
+### 3.1 Mac에 맞는 ZIP 선택
-macOS에서 ADB 및 Scrcpy는 [Homebrew](https://brew.sh/)를 통해 손쉽게 설치할 수 있습니다:
+- Apple M 시리즈 Mac: `DX-Manager-v-macos-arm64.zip`
+- Intel Mac: `DX-Manager-v-macos-x64.zip`
-```bash
-# Homebrew가 설치되어 있지 않은 경우 먼저 설치
-/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
-
-# scrcpy 및 android-platform-tools (adb) 설치
-brew install scrcpy android-platform-tools
-```
+여기서 ``은 공개된 Release 버전(예: `2.0.0`)으로 바꿉니다. ZIP 안의
+문서에는 패키징 시 실제 버전이 자동 반영됩니다.
-설치 후 터미널에서 정상 인식되는지 확인합니다:
+저장소의 GitHub Actions workflow는 `macos-15` Apple Silicon 실행 환경과
+`macos-15-intel` Intel 실행 환경에서 각각 전체 빌드·테스트·패키지 검증을
+수행하도록 구성되어 있습니다. 버전 태그의 두 작업이 성공하면 검증된 ZIP 두 개와
+SHA-256 파일을 포함한 GitHub Release 초안을 만들며, 유지관리자가 확인 후
+공개합니다. 이 변경의 첫 원격 workflow 성공 여부는 아직 확인해야 합니다.
+Release가 공개된 뒤 사용자는 Mac에 맞는 ZIP 하나를
+내려받아 전체 폴더의 압축을 풀고 `Start DX Manager.command`를 더블클릭합니다.
+소스 빌드는 필요하지 않습니다.
-```bash
-adb version
-scrcpy --version
-```
+현재 자동 생성 패키지는 Apple Developer ID 서명·공증 전 단계이므로 최초 실행
+시 macOS 승인이 필요할 수 있습니다. Control-클릭 후 **열기**를 사용하거나
+**시스템 설정 > 개인정보 보호 및 보안**에서 차단된 항목을 확인하십시오. 자동으로
+보안 속성을 지우는 명령은 실행하지 않습니다.
-### 3.2 .NET 8 SDK 설치
-
-```bash
-# Homebrew를 통한 설치
-brew install dotnet-sdk
-
-# 또는 Microsoft 공식 웹사이트 / dotnet-install 스크립트 이용
-# PATH 확인 (필요 시 ~/.zshrc 에 추가)
-export PATH="$HOME/.dotnet:$PATH"
-```
-
-### 3.3 스마트폰 설정
+### 3.2 스마트폰 설정
1. **개발자 옵션 활성화**:
- 휴대폰 **설정 > 휴대전화 정보 > 소프트웨어 정보**에서 **빌드번호**를 7회 연속 탭합니다.
@@ -72,9 +64,10 @@ export PATH="$HOME/.dotnet:$PATH"
- 데이터 전송이 가능한 USB-C 케이블로 Mac과 갤럭시 스마트폰을 연결합니다.
- 스마트폰 화면에 나타나는 **"이 컴퓨터에서 항상 디버깅을 허용합니까?"** 팝업에서 **항상 허용**을 체크하고 승인합니다.
----
+## 4. 소스 개발 및 포터블 패키징 (Development & Packaging)
-## 4. 프로젝트 빌드 및 테스트 (Build & Test)
+이 절은 프로그램을 수정하거나 배포 ZIP을 만드는 개발자용입니다. 일반 사용자는
+3절의 미리 빌드된 ZIP만 사용하면 됩니다.
### 4.1 솔루션 빌드
@@ -88,23 +81,45 @@ dotnet build DexManager.Mac.sln -c Release /warnaserror
### 4.2 테스트 스위트 실행
-DX Manager for macOS는 xUnit 기반의 단위/통합 테스트와 다중 기기 회귀 테스트를 완벽히 통과합니다:
+DX Manager for macOS는 xUnit 기반 단위/통합 테스트와 다중 기기 회귀 테스트를
+사용합니다.
```bash
-# 92개 xUnit 단위 및 통합 테스트 실행
+# 95개 xUnit 단위 및 통합 테스트 실행
dotnet test DexManager.Mac.sln -c Release
# 39개 다중 기기 세션 격리 회귀 테스트 실행
dotnet run --project DexManager.MultiDeviceTests -c Release
```
+### 4.3 아키텍처별 포터블 ZIP 생성
+
+```bash
+# Apple Silicon용 self-contained ZIP
+scripts/Package-Mac-Release.sh --rid osx-arm64
+
+# Intel용 self-contained ZIP
+scripts/Package-Mac-Release.sh --rid osx-x64
+```
+
+스크립트는 DX Manager와 ADB proxy를 지정한 RID로 미리 publish하고, 공식
+scrcpy 4.1 정적 빌드의 SHA-256을 확인한 뒤 번들합니다. 생성한 ZIP을 새 임시
+폴더에 다시 풀어 실행 권한, CPU 아키텍처, 외부 Homebrew 경로 의존성, 버전,
+라이선스와 사용자 데이터 제외 여부를 검사합니다.
+
---
## 5. 실행 및 CLI 명령어 (Usage & CLI Options)
### 5.1 대화형 콘솔 대시보드 (Interactive Dashboard) 실행
-인자 없이 실행하면 직관적인 컬러 TUI 대시보드가 시작됩니다:
+포터블 ZIP 사용자는 압축을 푼 폴더에서 다음 실행기를 더블클릭합니다.
+
+```bash
+./Start\ DX\ Manager.command
+```
+
+소스 개발자는 다음 명령 중 하나로 같은 대시보드를 실행할 수 있습니다.
```bash
dotnet run --project DexManager.Mac
@@ -126,11 +141,11 @@ dotnet run --project DexManager.Mac
예시:
```bash
-# DeX 즉시 실행
-dotnet run --project DexManager.Mac -- --dex
+# 포터블 ZIP에서 DeX 즉시 실행
+./DXManager.Mac --dex
# 시스템 및 기기 진단 리포트 출력
-dotnet run --project DexManager.Mac -- --diag
+./DXManager.Mac --diag
```
---
@@ -168,7 +183,7 @@ dotnet run --project DexManager.Mac -- --diag
1. **`[1] Start DeX Mode`**:
- 선택된 휴대폰에 가상 보조 디스플레이(Virtual Display Overlay)를 생성하고 Scrcpy를 통해 macOS 데스크톱에 삼성 덱스 화면을 엽니다.
2. **`[2] Stop DeX Mode`**:
- - 활성 DeX 세션의 Scrcpy를 정상 종료하고, 안드로이드 전역 `overlay_display_devices` 설정을 즉시 삭제하여 스마트폰의 가상 디스플레이 잔여물을 완전히 정리합니다.
+ - 활성 DeX 세션의 Scrcpy를 종료하고 `overlay_display_devices` 정리를 요청합니다. 휴대폰 연결이 유지되면 가상 디스플레이 제거 결과를 확인하며, 먼저 연결이 끊기면 정리를 확인하지 못할 수 있습니다.
3. **`[3] Start Single App Window`**:
- 가상 디스플레이 슬롯(1~3번)을 지정하고, 실행할 안드로이드 앱 패키지명(예: `com.sec.android.app.sbrowser`)을 입력하여 해당 앱만을 위한 독립된 가상 윈도우를 엽니다.
4. **`[4] Stop Single App Window`**:
@@ -182,13 +197,13 @@ dotnet run --project DexManager.Mac -- --diag
7. **`[7] Diagnostics & Environment`**:
- ADB, Scrcpy, .NET 8 런타임, 디바이스 SDK/One UI 버전, Companion 권한 상태를 종합 진단하여 점검 결과를 출력합니다.
8. **`[8] DX Companion Guardian`**:
- - 번들된 `DX-Companion.apk`의 서명과 해시를 검증 후 휴대폰에 설치하고, 가상 디스플레이 및 절전모드 해제 자동 복구를 위한 `WRITE_SECURE_SETTINGS` 권한을 부여합니다.
+ - 설치된 Companion의 상태와 권한을 확인합니다. 현재 macOS 공개 ZIP에는 Companion APK가 없으므로 자동 설치는 사용할 수 없습니다. 검증된 APK가 실제로 포함된 개발 빌드에서만 해시와 서명을 확인한 뒤 설치 메뉴가 동작합니다.
9. **`[9] Settings & Configuration`**:
- 가상 디스플레이 가로/세로 해상도, DPI(기본: 160~240), 스트리밍 비트레이트(예: 16M, 24M), 최대 FPS(60/120), 화면 끄기(TurnScreenOff), 절전모드 방지(StayAwake) 설정을 인터랙티브하게 변경하고 영구 저장합니다.
- **`[S] Select Active Device`**: 연결된 여러 대의 갤럭시 기기 중 제어할 대상 기기를 전환합니다.
- **`[L] View Recent Logs`**: 실시간 세션 로그 및 ADB 트랜잭션 기록을 확인합니다.
- **`[C] Clear Screen`**: 터미널 화면을 정리하고 배너와 대시보드를 다시 그립니다.
-- **`[Q] Exit`**: 모든 활성 세션과 가상 디스플레이를 안전하게 정리하고 프로그램을 종료합니다.
+- **`[Q] Exit`**: 활성 세션의 종료와 가상 디스플레이 정리를 요청하고 결과를 기다린 뒤 프로그램을 종료합니다. 휴대폰 연결이 먼저 끊기면 일부 정리를 확인하지 못했다는 메시지가 표시될 수 있습니다.
---
@@ -222,7 +237,11 @@ macOS 환경에서 Scrcpy 조작 시 기본 Modifier 키는 **`Option (⌥)`**
> 3. 또는 스마트폰 **설정 > 개발자 옵션 > 보조 디스플레이 시뮬레이션**에서 아무 해상도를 선택했다가 다시 **'없음'**을 선택합니다.
### Q2. `scrcpy`를 실행할 수 없다는 오류가 발생합니다.
-> **해결 방법**: `brew install scrcpy`가 정상 완료되었는지 확인하고, `which scrcpy` 명령으로 `/opt/homebrew/bin/scrcpy` 또는 `/usr/local/bin/scrcpy` 경로에 존재하는지 확인하십시오.
+> **포터블 ZIP 해결 방법**: `DXManager.Mac` 실행 파일만 따로 옮기지 않았는지
+> 확인하고 ZIP 전체를 새 폴더에 다시 푸십시오. 같은 폴더의
+> `tools/scrcpy/scrcpy`, `tools/scrcpy/adb`와 `scrcpy-server`가 모두 있어야
+> 합니다. Homebrew 설치는 포터블 ZIP의 해결 조건이 아닙니다. 소스 개발
+> 환경에서만 필요에 따라 PATH의 scrcpy를 fallback으로 사용할 수 있습니다.
### Q3. 기기 목록에 `unauthorized`로 표시됩니다.
> **해결 방법**: 스마트폰 화면을 켜고 잠금을 해제한 뒤, Mac에 대한 **"USB 디버깅을 항상 허용"** 팝업을 승인하십시오.
diff --git a/docs/PACKAGE_README_MACOS.md b/docs/PACKAGE_README_MACOS.md
new file mode 100644
index 0000000..d21d7f7
--- /dev/null
+++ b/docs/PACKAGE_README_MACOS.md
@@ -0,0 +1,223 @@
+# DX Manager for macOS
+
+DX Manager for macOS is distributed as a portable ZIP package. Extract the
+complete archive and run it from the extracted folder. The package includes the
+DX Manager runtime, scrcpy 4.1, ADB, and the scrcpy server. Homebrew and a
+separate .NET installation are not required.
+
+The portable packages require **macOS 14 Sonoma or later**.
+
+## English
+
+### 1. Choose the correct package
+
+- **Apple Silicon Mac:** choose the package labeled **arm64**. This applies to
+ Macs with an Apple M-series chip.
+- **Intel Mac:** choose the package labeled **x64** or **x86_64**.
+
+If you are not sure which Mac you have, open the Apple menu and select **About
+This Mac**. The window shows either an Apple chip or an Intel processor.
+
+The two packages provide the same DX Manager functions. Only their processor
+architecture is different.
+
+### 2. Verify the downloaded ZIP
+
+Download the matching `.zip.sha256` file from the same GitHub Release and keep
+it beside the ZIP. Before extracting, open Terminal in that folder and run:
+
+```bash
+release_version=""
+shasum -a 256 -c "DX-Manager-v${release_version}-macos-arm64.zip.sha256"
+```
+
+When reading this source document on GitHub, replace `` with the
+Release version shown in the downloaded filename, for example `2.0.0`. The
+README inside a packaged ZIP already contains the exact package version.
+For an Intel Mac, replace `arm64` with `x64`. Continue only when the command
+prints `OK`. If it reports a mismatch, do not run the ZIP; download both files
+again from the Release.
+
+### 3. Extract and keep the complete folder
+
+1. Extract the entire ZIP archive to a folder where your user account can write
+ files, such as **Downloads** or **Documents**.
+2. Keep all extracted files and subfolders together.
+3. Do not move only `Start DX Manager.command` or only the DX Manager
+ executable. The launcher uses the bundled runtime, scrcpy 4.1, ADB, and
+ related files from this folder.
+
+### 4. Prepare the Galaxy phone
+
+1. On the phone, open **Settings > About phone > Software information**.
+2. Tap **Build number** seven times to enable Developer options.
+3. Open **Settings > Developer options** and enable **USB debugging**.
+4. Unlock the phone and connect it to the Mac with a USB cable that supports
+ data transfer.
+5. When the phone displays **Allow USB debugging?**, verify the computer and
+ approve the RSA key. You may select **Always allow from this computer** if
+ this is your Mac.
+
+DX Manager cannot control a device shown as `unauthorized`. If the RSA prompt
+does not appear, reconnect the cable while the phone is unlocked and check the
+USB debugging setting again.
+
+### 5. Start DX Manager
+
+Double-click `Start DX Manager.command` in the extracted folder. A Terminal
+window opens and displays DX Manager's text menu.
+
+Use the menu as follows:
+
+- Press `1` to start DeX mode.
+- Press `2` to stop the active DeX session and remove the virtual display.
+- Press `Q` to exit DX Manager.
+
+Keep the phone connected while starting or stopping the session. Selecting `Q`
+also stops active DX Manager windows and requests removal of the DeX virtual
+display. Wait for the final cleanup message before disconnecting the cable. If
+the message says that a cleanup step could not be confirmed, reconnect the
+phone and use `2` before exiting again.
+
+### 6. First launch and Gatekeeper
+
+This package is not currently signed with an Apple Developer ID or notarized by
+Apple. On the first launch, macOS may block the launcher or display a warning.
+
+If you obtained the package from a source you trust, try either of these
+approval methods:
+
+- Control-click `Start DX Manager.command`, select **Open**, and confirm
+ **Open**.
+- After macOS blocks the launch, open **System Settings > Privacy & Security**
+ and use **Open Anyway** for the blocked item.
+
+The wording and location of these controls can differ by macOS version. Without
+Apple Developer ID signing and notarization, a completely warning-free launch
+on every supported Mac and macOS version cannot be guaranteed.
+
+### Package contents and limits
+
+- Bundled DX Manager runtime for the selected Mac architecture
+- Bundled scrcpy 4.1, ADB, and scrcpy server
+- No Homebrew requirement
+- No separate .NET installation requirement
+- No DX Companion APK in the current macOS package
+
+The Intel executable has been checked under Rosetta on Apple Silicon, while a
+complete Galaxy DeX session on physical Intel Mac hardware still needs direct
+verification.
+
+Do not add, replace, or delete bundled ADB and scrcpy files unless the package
+instructions for a later version explicitly require it.
+
+---
+
+## 한국어
+
+DX Manager macOS 버전은 포터블 ZIP 패키지로 배포됩니다. ZIP 전체를 풀고
+압축을 푼 폴더에서 실행하십시오. 패키지에는 DX Manager 런타임, scrcpy 4.1,
+ADB와 scrcpy 서버가 포함됩니다. Homebrew와 별도의 .NET 설치는 필요하지
+않습니다.
+
+포터블 패키지의 최소 운영체제는 **macOS 14 Sonoma**입니다.
+
+### 1. Mac에 맞는 패키지 선택
+
+- **Apple Silicon Mac:** **arm64**로 표시된 패키지를 선택하십시오. Apple M
+ 시리즈 칩이 탑재된 Mac이 여기에 해당합니다.
+- **Intel Mac:** **x64** 또는 **x86_64**로 표시된 패키지를 선택하십시오.
+
+Mac의 종류를 모르면 Apple 메뉴에서 **이 Mac에 관하여**를 여십시오. 이 화면에
+Apple 칩 또는 Intel 프로세서가 표시됩니다.
+
+두 패키지의 DX Manager 기능은 같습니다. 프로세서 아키텍처만 다릅니다.
+
+### 2. 다운로드한 ZIP 확인
+
+같은 GitHub Release에서 ZIP과 이름이 같은 `.zip.sha256` 파일도 내려받아 ZIP
+옆에 둡니다. 압축을 풀기 전에 해당 폴더에서 터미널을 열고 다음 명령을
+실행하십시오.
+
+```bash
+release_version=""
+shasum -a 256 -c "DX-Manager-v${release_version}-macos-arm64.zip.sha256"
+```
+
+이 원본 문서를 GitHub에서 읽는 경우 ``을 내려받은 파일명에 표시된
+Release 버전(예: `2.0.0`)으로 바꾸십시오. 패키지 ZIP 안의 README에는 실제
+패키지 버전이 이미 반영됩니다.
+Intel Mac에서는 `arm64`를 `x64`로 바꿉니다. 결과에 `OK`가 표시된 경우에만
+계속하십시오. 불일치가 표시되면 실행하지 말고 Release에서 두 파일을 다시
+내려받으십시오.
+
+### 3. ZIP 전체 압축 해제 및 폴더 유지
+
+1. ZIP 전체를 **다운로드** 또는 **문서**처럼 사용자 계정이 파일을 쓸 수
+ 있는 폴더에 압축 해제하십시오.
+2. 압축을 푼 모든 파일과 하위 폴더를 한 폴더 안에 그대로 유지하십시오.
+3. `Start DX Manager.command` 또는 DX Manager 실행 파일만 따로 옮기지
+ 마십시오. 실행기는 같은 폴더에 포함된 런타임, scrcpy 4.1, ADB와 관련
+ 파일을 사용합니다.
+
+### 4. Galaxy 휴대폰 준비
+
+1. 휴대폰에서 **설정 > 휴대전화 정보 > 소프트웨어 정보**를 여십시오.
+2. **빌드번호**를 일곱 번 눌러 개발자 옵션을 활성화하십시오.
+3. **설정 > 개발자 옵션**에서 **USB 디버깅**을 켜십시오.
+4. 휴대폰 잠금을 해제하고 데이터 전송을 지원하는 USB 케이블로 Mac과
+ 연결하십시오.
+5. 휴대폰에 **USB 디버깅을 허용하시겠습니까?**가 표시되면 연결된 컴퓨터를
+ 확인한 뒤 RSA 키를 승인하십시오. 본인의 Mac이면 **이 컴퓨터에서 항상
+ 허용**을 선택할 수 있습니다.
+
+장치 상태가 `unauthorized`이면 DX Manager에서 장치를 제어할 수 없습니다.
+RSA 승인 화면이 나타나지 않으면 휴대폰 잠금이 해제된 상태에서 케이블을 다시
+연결하고 USB 디버깅 설정을 확인하십시오.
+
+### 5. DX Manager 시작
+
+압축을 푼 폴더의 `Start DX Manager.command`를 더블클릭하십시오. 터미널 창이
+열리고 DX Manager의 텍스트 메뉴가 표시됩니다.
+
+메뉴는 다음과 같이 사용합니다.
+
+- `1`: DeX 모드를 시작합니다.
+- `2`: 실행 중인 DeX 세션을 중지하고 가상 디스플레이를 제거합니다.
+- `Q`: DX Manager를 종료합니다.
+
+세션을 시작하거나 중지하는 동안에는 휴대폰 연결을 유지하십시오. `Q`를
+선택해도 DX Manager가 실행한 창을 중지하고 DeX 가상 디스플레이 제거를
+요청합니다. 마지막 정리 완료 메시지를 확인한 뒤 케이블을 분리하십시오. 일부
+정리 단계를 확인하지 못했다는 메시지가 나오면 휴대폰을 다시 연결하고 `2`로
+중지한 뒤 다시 종료하십시오.
+
+### 6. 최초 실행 및 Gatekeeper
+
+현재 패키지는 Apple Developer ID 서명과 공증이 적용되지 않은 빌드입니다. 최초
+실행 시 macOS가 실행을 차단하거나 경고를 표시할 수 있습니다.
+
+신뢰하는 경로에서 받은 패키지라면 다음 승인 방법 중 하나를 시도하십시오.
+
+- `Start DX Manager.command`를 Control-클릭하고 **열기**를 선택한 뒤 다시
+ **열기**를 확인합니다.
+- macOS가 실행을 차단한 뒤 **시스템 설정 > 개인정보 보호 및 보안**을 열고
+ 차단된 항목에 대해 **확인 없이 열기** 또는 **그래도 열기**를 선택합니다.
+
+macOS 버전에 따라 메뉴 문구와 위치가 다를 수 있습니다. Apple Developer ID
+서명과 공증이 없으면 지원 대상의 모든 Mac과 macOS 버전에서 경고가 전혀 없는
+실행을 보장할 수 없습니다.
+
+### 패키지 구성 및 범위
+
+- 선택한 Mac 아키텍처용 DX Manager 런타임
+- 번들 scrcpy 4.1, ADB와 scrcpy 서버
+- Homebrew 설치 불필요
+- 별도 .NET 설치 불필요
+- 현재 macOS 패키지에는 DX Companion APK가 포함되지 않음
+
+Intel 실행 파일은 Apple Silicon Mac의 Rosetta에서 기동을 확인했습니다. 실제
+Intel Mac에서 Galaxy DeX 전체 흐름을 확인하는 실기 검증은 남아 있습니다.
+
+이후 버전의 패키지 안내에서 명시적으로 요구하지 않는 한 번들 ADB와 scrcpy
+파일을 추가, 교체 또는 삭제하지 마십시오.
diff --git a/docs/PROJECT_BRIEF.md b/docs/PROJECT_BRIEF.md
index c2497ad..207a92c 100644
--- a/docs/PROJECT_BRIEF.md
+++ b/docs/PROJECT_BRIEF.md
@@ -28,6 +28,12 @@ GitHub 메인 README와 별도로 HTML이 없는 `docs\PACKAGE_README.md`를 배
폴더의 `README.md`로 사용하며, 모든 이중 언어 문서는 영어 다음 한국어
순서로 작성한다.
+macOS 에디션은 `scripts/Package-Mac-Release.sh`로 Apple Silicon arm64와
+Intel x64 self-contained ZIP을 별도로 만든다. 각 ZIP에는 해당 아키텍처로
+미리 publish한 DX Manager와 ADB proxy, 공식 scrcpy 4.1 정적 빌드, ADB,
+scrcpy-server, 실행기와 라이선스를 포함한다. 사용자는 Homebrew나 .NET을
+설치하거나 소스를 빌드하지 않고 ZIP 전체를 풀어 실행한다.
+
## 완료 기능
- DeX overlay 가상 디스플레이 생성, 재사용, 초기화
diff --git a/docs/README.md b/docs/README.md
index 5dcd8c5..c6f1267 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -19,6 +19,7 @@
- `RELEASE_NOTES_v2.0.0.md`: v2.0.0 GitHub Release용 영어·한국어 설명
- `RELEASE_NOTES_v1.3.0.md`: 이전 v1.3.0 GitHub Release 기록
- `PACKAGE_README.md`: 배포 ZIP 루트에 들어가는 HTML 없는 영어·한국어 안내
+- `PACKAGE_README_MACOS.md`: macOS arm64/x64 포터블 ZIP 전용 영어·한국어 안내
- `../DXDisplayCleanup`: 번들 선택형 Android 복구·파일 전송 앱 소스와 빌드 문서
사용자용 README는 저장소 루트의 `README.md`, 영어/한국어 설명서는
diff --git a/docs/SESSION.md b/docs/SESSION.md
index af27afe..4c4bb99 100644
--- a/docs/SESSION.md
+++ b/docs/SESSION.md
@@ -1,6 +1,36 @@
# Session Handoff
-마지막 갱신: 2026-08-22
+마지막 갱신: 2026-08-31
+
+## macOS 포터블 배포 작업
+
+- 작업 브랜치: `codex/macos-portable-release`
+- `scripts/Package-Mac-Release.sh`가 `osx-arm64`와 `osx-x64`용 DX Manager와
+ ADB proxy를 self-contained single-file로 publish한다.
+- 공식 scrcpy 4.1 arm64/x86_64 정적 아카이브를 고정 SHA-256으로 검증해
+ ADB와 scrcpy-server를 함께 포함한다.
+- `DX-Manager-v2.0.0-macos-arm64.zip`과
+ `DX-Manager-v2.0.0-macos-x64.zip`, 각 `.sha256` 파일을 생성한다.
+- GitHub Actions workflow는 `macos-15`와 `macos-15-intel`에서 빌드·95개
+ 테스트·39개 다중 기기 테스트·package smoke test를 각각 수행하도록
+ 구성했다. PR에서는 artifact를 보관하고, `v*` 태그에서는 정확한 네 파일과
+ 체크섬을 검증한 뒤 Release 초안을 만든다. 첫 원격 run 결과 확인은 남아 있다.
+- Apple Silicon ZIP은 native 실행 검증을 통과했다. Intel ZIP은 Mach-O x64
+ 검사와 이 Mac의 Rosetta에서 DX Manager, proxy, scrcpy 4.1, ADB 37.0.0
+ 실행을 확인했다. 실제 Intel Mac의 Galaxy DeX 실기는 남아 있다.
+- macOS `Q` 종료가 no-op이던 조건을 제거하고 실행 중인 서비스 정지와 DeX
+ overlay cleanup 완료를 기다리도록 수정했다. CLI `--dex`/`--stop-dex`도 기기
+ 감시가 첫 snapshot을 읽은 뒤 동작하며 시작·자연 종료 대기의 `Ctrl+C`를 실제
+ 정리 경로에 연결한다.
+- DeX 시작·overlay 제거 직전에 live stable identity를 다시 읽는다. 같은 endpoint의
+ 다른 휴대폰에는 정리 명령을 보내지 않고, USB transport가 사라져도 같은
+ identity로 검증된 Wi-Fi transport가 남아 있으면 그 경로로 정리한다. 보류된
+ cleanup은 identity별 독립 항목으로 보존하고 새 overlay 설정이 성공한 뒤에만
+ 완료 처리한다.
+- 최종 소스 기준 .NET 8 Release 빌드는 경고 0·오류 0, xUnit 95개와 다중 기기
+ 회귀 테스트 39개는 모두 통과했다.
+- 자동 artifact는 아직 Apple Developer ID 서명·notarization이 없으므로 최초
+ 실행 시 Gatekeeper 승인이 필요할 수 있다.
## macOS 크로스플랫폼 변환 작업 현황 (v2.0.0 macOS Edition)
@@ -9,7 +39,7 @@
- `DexManager.Mac` (.NET 8.0): macOS 전용 ANSI 컬러 콘솔 호스트 (`InteractiveHost`), TUI 대시보드, 네이티브 플랫폼 서비스 (`MacPlatformService`, `MacPathProvider`, `MacCaptureService`, `MacKeyboardService`, `MacAutoStartService`) 구현 완료
- `DexManager.AdbProxy` (.NET 8.0): Named Pipe 기반 Scrcpy 파일 드롭 가로채기 및 관리형 전송 중계 구현 완료
- **2단계 (테스트 및 검증)**:
- - `DexManager.Tests` (net8.0, xUnit 2.5.3): 92개 단위/통합 테스트 100% 통과
+ - `DexManager.Tests` (net8.0, xUnit 2.5.3): 95개 단위/통합 테스트 100% 통과
- `DexManager.MultiDeviceTests` (net8.0): 39개 다중 기기 세션 격리 회귀 테스트 100% 통과
- **3단계 (리팩터링, 품질 개선, 문서화)**:
- C# 12 / .NET 8 최신 문법(파일 범위 네임스페이스, 패턴 매칭, 레코드, 컬렉션 식, 식 본문 멤버) 적용
diff --git a/docs/TODO.md b/docs/TODO.md
index bac6b95..247cd06 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -2,6 +2,18 @@
## 다음 작업
+- [x] macOS 아키텍처별 포터블 배포 기반
+ - [x] arm64/x64 DX Manager와 ADB proxy self-contained single-file publish
+ - [x] 공식 scrcpy 4.1 arm64/x86_64 아카이브 SHA-256 검증 및 번들
+ - [x] ZIP 재압축 해제, 실행 권한, 아키텍처, 외부 경로, 제외 파일 검증
+ - [x] Apple Silicon/x64 GitHub Actions matrix, PR artifact와 검증된 Release 초안
+ - [ ] PR에서 첫 GitHub Actions arm64/x64 원격 실행 결과 확인
+ - [x] `Q` 정상 종료의 DeX·단일창·전송·overlay cleanup 보강
+ - [x] DeX 시작·정리 전 live identity 재검증과 identity별 보류 정리 분리
+ - [x] CLI 시작·자연 종료 대기의 `Ctrl+C` 취소 및 종료 경합 보강
+ - [ ] 실제 Intel Mac에서 Galaxy DeX 시작·중지와 overlay cleanup 실기 확인
+ - [ ] Apple Developer ID 서명·notarization 자격이 준비되면 공개 ZIP 서명
+
- [x] v2.0.0 공개 후보 준비
- [x] Windows 버전 2.0.0과 DX Companion 2.0.0(versionCode 6) 일치
- [x] 다중 기기 README·사용 설명서·FAQ·개발 문서 최신화
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..51bb0f3
--- /dev/null
+++ b/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "8.0.130",
+ "rollForward": "disable",
+ "allowPrerelease": false
+ }
+}
diff --git a/scripts/Package-Mac-Release.sh b/scripts/Package-Mac-Release.sh
new file mode 100755
index 0000000..011ff2c
--- /dev/null
+++ b/scripts/Package-Mac-Release.sh
@@ -0,0 +1,471 @@
+#!/bin/bash
+
+set -euo pipefail
+
+script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+repository_root="$(cd "$script_directory/.." && pwd)"
+
+version="2.0.0"
+rid=""
+output_directory="$repository_root/dist"
+scrcpy_archive=""
+skip_tests=0
+
+scrcpy_version="4.1"
+dotnet_runtime_version="8.0.30"
+dotnet_license_url="https://raw.githubusercontent.com/dotnet/runtime/v8.0.30/LICENSE.TXT"
+dotnet_license_sha256="cfc21f5e8bd655ae997eec916138b707b1d290b83272c02a95c9f821b8c87310"
+dotnet_notices_url="https://raw.githubusercontent.com/dotnet/runtime/v8.0.30/THIRD-PARTY-NOTICES.TXT"
+dotnet_notices_sha256="97c1a7b3da6a4c6ad516448719f45114b41a4d4c5aa300a944476e2e4f5da438"
+
+usage() {
+ cat <<'EOF'
+Create one prebuilt, self-contained DX Manager portable ZIP for macOS.
+
+Usage:
+ scripts/Package-Mac-Release.sh --rid osx-arm64|osx-x64 [options]
+
+Options:
+ --version VERSION Package version (default: 2.0.0)
+ --output-dir DIRECTORY Generated ZIP directory (default: ./dist)
+ --scrcpy-archive FILE Use an already downloaded official scrcpy archive
+ --skip-tests Skip the test suites (intended only after CI tests)
+ -h, --help Show this help
+
+Examples:
+ scripts/Package-Mac-Release.sh --rid osx-arm64
+ scripts/Package-Mac-Release.sh --rid osx-x64 --version 2.0.0
+EOF
+}
+
+fail() {
+ echo "Package-Mac-Release: $*" >&2
+ exit 1
+}
+
+require_command() {
+ command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
+}
+
+sha256_file() {
+ shasum -a 256 "$1" | awk '{print $1}'
+}
+
+verify_sha256() {
+ local file_path="$1"
+ local expected="$2"
+ local description="$3"
+ local actual
+ actual="$(sha256_file "$file_path")"
+ [[ "$actual" == "$expected" ]] ||
+ fail "$description SHA-256 mismatch: expected $expected, got $actual"
+}
+
+download_verified() {
+ local url="$1"
+ local destination="$2"
+ local expected="$3"
+ local description="$4"
+ curl --fail --location --retry 3 --silent --show-error \
+ "$url" --output "$destination"
+ verify_sha256 "$destination" "$expected" "$description"
+}
+
+assert_architecture() {
+ local binary="$1"
+ local expected="$2"
+ local description="$3"
+ local architectures
+ architectures="$(lipo -archs "$binary" 2>/dev/null || true)"
+ case " $architectures " in
+ *" $expected "*) ;;
+ *) fail "$description architecture is '$architectures', expected '$expected': $binary" ;;
+ esac
+}
+
+assert_portable_dependencies() {
+ local binary="$1"
+ local dependencies
+ dependencies="$(otool -L "$binary" | grep '^[[:space:]]' || true)"
+ if printf '%s\n' "$dependencies" | grep -E '/opt/homebrew|/usr/local|/Users/|/private/tmp|/var/folders/' >/dev/null; then
+ fail "build-machine dependency found in $binary:\n$dependencies"
+ fi
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --rid)
+ [[ $# -ge 2 ]] || fail "--rid requires a value"
+ rid="$2"
+ shift 2
+ ;;
+ --version)
+ [[ $# -ge 2 ]] || fail "--version requires a value"
+ version="$2"
+ shift 2
+ ;;
+ --output-dir)
+ [[ $# -ge 2 ]] || fail "--output-dir requires a value"
+ output_directory="$2"
+ shift 2
+ ;;
+ --scrcpy-archive)
+ [[ $# -ge 2 ]] || fail "--scrcpy-archive requires a value"
+ scrcpy_archive="$2"
+ shift 2
+ ;;
+ --skip-tests)
+ skip_tests=1
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ fail "unknown argument: $1"
+ ;;
+ esac
+done
+
+[[ "$(uname -s)" == "Darwin" ]] || fail "this packaging script must run on macOS"
+[[ -n "$rid" ]] || fail "--rid is required"
+[[ "$version" =~ ^[0-9A-Za-z][0-9A-Za-z._-]*$ ]] ||
+ fail "version contains unsupported filename characters: $version"
+
+case "$rid" in
+ osx-arm64)
+ package_arch="arm64"
+ binary_arch="arm64"
+ scrcpy_asset_arch="aarch64"
+ scrcpy_sha256="20fd47c9014dd5e0fa77091f3cb7adbda8445a360c4584aeaa0150b5b3988ff3"
+ ;;
+ osx-x64)
+ package_arch="x64"
+ binary_arch="x86_64"
+ scrcpy_asset_arch="x86_64"
+ scrcpy_sha256="ee2a7223bc8dbdc4f482db1134bcf441178dafb833492b71ca4c22090c58ce72"
+ ;;
+ *)
+ fail "unsupported RID '$rid'; use osx-arm64 or osx-x64"
+ ;;
+esac
+
+require_command dotnet
+require_command curl
+require_command shasum
+require_command tar
+require_command lipo
+require_command otool
+require_command codesign
+require_command ditto
+require_command unzip
+require_command zip
+require_command date
+require_command sed
+
+output_directory="$(mkdir -p "$output_directory" && cd "$output_directory" && pwd)"
+temporary_base="${TMPDIR:-/tmp}"
+temporary_base="${temporary_base%/}"
+work_root="$(mktemp -d "$temporary_base/dx-manager-macos-package.XXXXXX")"
+output_staging=""
+
+source_date_epoch="${SOURCE_DATE_EPOCH:-}"
+if [[ -z "$source_date_epoch" ]] && command -v git >/dev/null 2>&1; then
+ source_date_epoch="$(git -C "$repository_root" log -1 --format=%ct 2>/dev/null || true)"
+fi
+if [[ -z "$source_date_epoch" ]]; then
+ source_date_epoch="946684800"
+fi
+[[ "$source_date_epoch" =~ ^[0-9]+$ ]] ||
+ fail "SOURCE_DATE_EPOCH must contain Unix epoch seconds: $source_date_epoch"
+normalized_timestamp="$(date -u -r "$source_date_epoch" +%Y%m%d%H%M.%S)"
+
+cleanup() {
+ case "$work_root" in
+ "$temporary_base"/dx-manager-macos-package.*)
+ rm -rf -- "$work_root"
+ ;;
+ esac
+ case "${output_staging:-}" in
+ "$output_directory"/.dx-manager-macos-output.*)
+ rm -rf -- "$output_staging"
+ ;;
+ esac
+}
+trap cleanup EXIT
+output_staging="$(mktemp -d "$output_directory/.dx-manager-macos-output.XXXXXX")"
+
+if [[ $skip_tests -eq 0 ]]; then
+ dotnet test "$repository_root/DexManager.Tests/DexManager.Tests.csproj" \
+ --configuration Release --nologo
+ dotnet run --project "$repository_root/DexManager.MultiDeviceTests/DexManager.MultiDeviceTests.csproj" \
+ --configuration Release
+fi
+
+main_publish="$work_root/publish-main"
+proxy_publish="$work_root/publish-proxy"
+
+dotnet publish "$repository_root/DexManager.Mac/DexManager.Mac.csproj" \
+ --configuration Release \
+ --framework net8.0 \
+ --runtime "$rid" \
+ --self-contained true \
+ --output "$main_publish" \
+ -p:Version="$version" \
+ -p:RuntimeFrameworkVersion="$dotnet_runtime_version" \
+ -p:TreatWarningsAsErrors=true \
+ -p:CopyBundledMacTools=false \
+ -p:PublishSingleFile=true \
+ -p:IncludeNativeLibrariesForSelfExtract=true \
+ -p:EnableCompressionInSingleFile=false \
+ -p:PublishTrimmed=false \
+ -p:DebugSymbols=false \
+ -p:DebugType=None
+
+dotnet publish "$repository_root/DexManager.AdbProxy/DexManager.AdbProxy.csproj" \
+ --configuration Release \
+ --framework net8.0 \
+ --runtime "$rid" \
+ --self-contained true \
+ --output "$proxy_publish" \
+ -p:Version="$version" \
+ -p:RuntimeFrameworkVersion="$dotnet_runtime_version" \
+ -p:TreatWarningsAsErrors=true \
+ -p:PublishSingleFile=true \
+ -p:IncludeNativeLibrariesForSelfExtract=true \
+ -p:EnableCompressionInSingleFile=false \
+ -p:PublishTrimmed=false \
+ -p:DebugSymbols=false \
+ -p:DebugType=None
+
+[[ -f "$main_publish/DXManager.Mac" ]] || fail "DXManager.Mac publish output is missing"
+[[ -f "$proxy_publish/DXMAdbProxy" ]] || fail "DXMAdbProxy publish output is missing"
+
+scrcpy_archive_name="scrcpy-macos-$scrcpy_asset_arch-v$scrcpy_version.tar.gz"
+scrcpy_download="$work_root/$scrcpy_archive_name"
+if [[ -n "$scrcpy_archive" ]]; then
+ [[ -f "$scrcpy_archive" ]] || fail "scrcpy archive not found: $scrcpy_archive"
+ cp "$scrcpy_archive" "$scrcpy_download"
+ verify_sha256 "$scrcpy_download" "$scrcpy_sha256" "scrcpy $scrcpy_version $package_arch archive"
+else
+ scrcpy_url="https://github.com/Genymobile/scrcpy/releases/download/v$scrcpy_version/$scrcpy_archive_name"
+ download_verified "$scrcpy_url" "$scrcpy_download" "$scrcpy_sha256" \
+ "scrcpy $scrcpy_version $package_arch archive"
+fi
+
+scrcpy_extract="$work_root/scrcpy"
+mkdir -p "$scrcpy_extract"
+tar -xzf "$scrcpy_download" -C "$scrcpy_extract"
+scrcpy_root="$scrcpy_extract/scrcpy-macos-$scrcpy_asset_arch-v$scrcpy_version"
+[[ -d "$scrcpy_root" ]] || fail "official scrcpy archive has an unexpected directory layout"
+for required in scrcpy scrcpy-server adb LICENSE; do
+ [[ -f "$scrcpy_root/$required" ]] || fail "official scrcpy archive is missing: $required"
+done
+
+package_parent="$work_root/package"
+package_root="$package_parent/DX Manager"
+mkdir -p "$package_root/tools/scrcpy" "$package_root/tools/adb-proxy" \
+ "$package_root/config" "$package_root/licenses" "$package_root/docs"
+
+cp "$main_publish/DXManager.Mac" "$package_root/DXManager.Mac"
+cp "$proxy_publish/DXMAdbProxy" "$package_root/tools/adb-proxy/DXMAdbProxy"
+cp -R "$scrcpy_root/." "$package_root/tools/scrcpy/"
+cp "$repository_root/build/macos/Start DX Manager.command" \
+ "$package_root/Start DX Manager.command"
+cp "$repository_root/build/macos/PORTABLE_PACKAGE.txt" \
+ "$package_root/PORTABLE_PACKAGE.txt"
+sed "s//$version/g" \
+ "$repository_root/docs/PACKAGE_README_MACOS.md" > "$package_root/README.md"
+sed "s//$version/g" \
+ "$repository_root/docs/MACOS_GUIDE.md" > "$package_root/docs/MACOS_GUIDE.md"
+cp "$repository_root/DexManager/config/README.txt" "$package_root/config/README.txt"
+cp "$repository_root/LICENSE" "$package_root/LICENSE"
+cp "$repository_root/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md" \
+ "$package_root/licenses/THIRD_PARTY_NOTICES.md"
+cp "$repository_root/DexManager/licenses/scrcpy-LICENSE.txt" "$package_root/licenses/"
+cp "$repository_root/DexManager/licenses/LGPL-2.1-LICENSE.txt" "$package_root/licenses/"
+cp "$repository_root/DexManager/licenses/SDL3-LICENSE.txt" "$package_root/licenses/"
+cp "$repository_root/DexManager/licenses/zlib-LICENSE.txt" "$package_root/licenses/"
+cp "$repository_root/DexManager/licenses/dav1d-LICENSE.txt" "$package_root/licenses/"
+
+dotnet_license="$work_root/dotnet-LICENSE.txt"
+dotnet_notices="$work_root/dotnet-THIRD-PARTY-NOTICES.txt"
+download_verified "$dotnet_license_url" "$dotnet_license" \
+ "$dotnet_license_sha256" ".NET Runtime $dotnet_runtime_version license"
+download_verified "$dotnet_notices_url" "$dotnet_notices" \
+ "$dotnet_notices_sha256" ".NET Runtime $dotnet_runtime_version third-party notices"
+cp "$dotnet_license" "$package_root/licenses/dotnet-LICENSE.txt"
+cp "$dotnet_notices" "$package_root/licenses/dotnet-THIRD-PARTY-NOTICES.txt"
+
+find "$package_root" -type d -exec chmod 755 {} +
+find "$package_root" -type f -exec chmod 644 {} +
+chmod 755 "$package_root/DXManager.Mac" \
+ "$package_root/Start DX Manager.command" \
+ "$package_root/tools/adb-proxy/DXMAdbProxy" \
+ "$package_root/tools/scrcpy/scrcpy" \
+ "$package_root/tools/scrcpy/adb"
+
+for required in \
+ "DXManager.Mac" \
+ "Start DX Manager.command" \
+ "PORTABLE_PACKAGE.txt" \
+ "README.md" \
+ "LICENSE" \
+ "tools/adb-proxy/DXMAdbProxy" \
+ "tools/scrcpy/scrcpy" \
+ "tools/scrcpy/scrcpy-server" \
+ "tools/scrcpy/adb" \
+ "licenses/THIRD_PARTY_NOTICES.md" \
+ "licenses/dotnet-LICENSE.txt" \
+ "licenses/dotnet-THIRD-PARTY-NOTICES.txt"; do
+ [[ -f "$package_root/$required" ]] || fail "required package file is missing: $required"
+done
+
+if grep -F '' "$package_root/README.md" \
+ "$package_root/docs/MACOS_GUIDE.md" >/dev/null; then
+ fail "package documentation still contains an unresolved version placeholder"
+fi
+
+unexpected="$(find "$package_root" \
+ \( -name 'settings.json*' -o -name '*.pdb' -o -name '*.dSYM' \
+ -o -name '.DS_Store' -o -name '*.keystore' -o -name 'signing.properties' \
+ -o -name '*.cs' -o -name '*.csproj' -o -name '*.sln' \) \
+ -print -quit)"
+[[ -z "$unexpected" ]] || fail "private, source, or debug file found in package: $unexpected"
+
+symlink="$(find "$package_root" -type l -print -quit)"
+[[ -z "$symlink" ]] || fail "symbolic links are not allowed in the portable package: $symlink"
+
+assert_architecture "$package_root/DXManager.Mac" "$binary_arch" "DX Manager"
+assert_architecture "$package_root/tools/adb-proxy/DXMAdbProxy" "$binary_arch" "ADB proxy"
+assert_architecture "$package_root/tools/scrcpy/scrcpy" "$binary_arch" "scrcpy"
+assert_architecture "$package_root/tools/scrcpy/adb" "$binary_arch" "ADB"
+
+assert_portable_dependencies "$package_root/DXManager.Mac"
+assert_portable_dependencies "$package_root/tools/adb-proxy/DXMAdbProxy"
+assert_portable_dependencies "$package_root/tools/scrcpy/scrcpy"
+assert_portable_dependencies "$package_root/tools/scrcpy/adb"
+
+for signed_binary in \
+ "$package_root/DXManager.Mac" \
+ "$package_root/tools/adb-proxy/DXMAdbProxy"; do
+ codesign --force --sign - --timestamp=none "$signed_binary"
+ codesign --verify --strict "$signed_binary"
+done
+
+for upstream_binary in \
+ "$package_root/tools/scrcpy/scrcpy" \
+ "$package_root/tools/scrcpy/adb"; do
+ if ! codesign --verify --strict "$upstream_binary" 2>/dev/null; then
+ codesign --force --sign - --timestamp=none "$upstream_binary"
+ fi
+ codesign --verify --strict "$upstream_binary"
+done
+
+host_arch="$(uname -m)"
+if [[ "${CI:-}" == "true" && "$host_arch" != "$binary_arch" ]]; then
+ fail "CI runner architecture is $host_arch, expected $binary_arch for $rid"
+fi
+if [[ "$host_arch" == "$binary_arch" ]]; then
+ smoke_home="$work_root/smoke-home"
+ mkdir -p "$smoke_home"
+ clean_path="/usr/bin:/bin:/usr/sbin:/sbin"
+
+ version_output="$(HOME="$smoke_home" PATH="$clean_path" \
+ "$package_root/DXManager.Mac" --version)"
+ [[ "$version_output" == *"$version"* ]] ||
+ fail "DX Manager version output does not contain $version: $version_output"
+ HOME="$smoke_home" PATH="$clean_path" "$package_root/DXManager.Mac" --help >/dev/null
+ proxy_output="$(HOME="$smoke_home" PATH="$clean_path" \
+ "$package_root/tools/adb-proxy/DXMAdbProxy" --self-test)"
+ [[ "$proxy_output" == *"$version"* ]] ||
+ fail "ADB proxy self-test output does not contain $version: $proxy_output"
+ scrcpy_output="$(HOME="$smoke_home" PATH="$clean_path" \
+ "$package_root/tools/scrcpy/scrcpy" --version 2>&1)"
+ [[ "$scrcpy_output" == *"scrcpy $scrcpy_version"* ]] ||
+ fail "scrcpy version verification failed: $scrcpy_output"
+ HOME="$smoke_home" PATH="$clean_path" \
+ "$package_root/tools/scrcpy/adb" version >/dev/null
+else
+ echo "Skipping executable smoke tests: host is $host_arch, package is $binary_arch."
+ echo "The matching GitHub Actions runner performs these tests before artifact upload."
+fi
+
+zip_path="$output_directory/DX-Manager-v$version-macos-$package_arch.zip"
+checksum_path="$zip_path.sha256"
+staged_zip="$output_staging/$(basename "$zip_path")"
+staged_checksum="$output_staging/$(basename "$checksum_path")"
+find "$package_root" -exec touch -h -t "$normalized_timestamp" {} +
+(
+ cd "$package_parent"
+ find "DX Manager" -print | LC_ALL=C sort | \
+ zip -X -q "$staged_zip" -@
+)
+
+archive_metadata="$(unzip -Z1 "$staged_zip" | \
+ grep -E '(^|/)\._|^__MACOSX/' | head -n 1 || true)"
+[[ -z "$archive_metadata" ]] ||
+ fail "macOS metadata file found in portable ZIP: $archive_metadata"
+
+extract_root="$work_root/reextract"
+mkdir -p "$extract_root"
+ditto -x -k --norsrc --noextattr --noqtn --noacl \
+ "$staged_zip" "$extract_root"
+reextracted="$extract_root/DX Manager"
+[[ -x "$reextracted/DXManager.Mac" ]] || fail "ZIP did not preserve the DX Manager executable bit"
+[[ -x "$reextracted/Start DX Manager.command" ]] || fail "ZIP did not preserve the launcher executable bit"
+[[ -x "$reextracted/tools/adb-proxy/DXMAdbProxy" ]] || fail "ZIP did not preserve the proxy executable bit"
+[[ -x "$reextracted/tools/scrcpy/scrcpy" ]] || fail "ZIP did not preserve the scrcpy executable bit"
+[[ -x "$reextracted/tools/scrcpy/adb" ]] || fail "ZIP did not preserve the ADB executable bit"
+
+if [[ "$host_arch" == "$binary_arch" ]]; then
+ extracted_smoke_home="$work_root/extracted-smoke-home"
+ extracted_smoke_tmp="$work_root/extracted-smoke-tmp"
+ mkdir -p "$extracted_smoke_home" "$extracted_smoke_tmp"
+ launcher_version="$(HOME="$extracted_smoke_home" \
+ TMPDIR="$extracted_smoke_tmp" \
+ PATH="/usr/bin:/bin:/usr/sbin:/sbin" \
+ "$reextracted/Start DX Manager.command" --version 2>&1)"
+ [[ "$launcher_version" == *"$version"* ]] ||
+ fail "re-extracted launcher version check failed: $launcher_version"
+ tui_output="$({ sleep 2; printf 'Q\n'; } | \
+ HOME="$extracted_smoke_home" \
+ TMPDIR="$extracted_smoke_tmp" \
+ PATH="/usr/bin:/bin:/usr/sbin:/sbin" \
+ "$reextracted/Start DX Manager.command" 2>&1)"
+ [[ "$tui_output" == *"DX Manager stopped cleanly"* ]] ||
+ fail "re-extracted TUI did not complete Q cleanup: $tui_output"
+fi
+
+zip_hash="$(sha256_file "$staged_zip")"
+printf '%s %s\n' "$zip_hash" "$(basename "$zip_path")" > "$staged_checksum"
+
+previous_zip="$output_staging/previous.zip"
+previous_checksum="$output_staging/previous.zip.sha256"
+if [[ -e "$zip_path" || -e "$checksum_path" ]]; then
+ [[ -f "$zip_path" && -f "$checksum_path" ]] ||
+ fail "existing package output is incomplete; keep or remove the ZIP and checksum together"
+ mv -- "$zip_path" "$previous_zip" ||
+ fail "could not stage the previous portable ZIP for replacement"
+ if ! mv -- "$checksum_path" "$previous_checksum"; then
+ mv -- "$previous_zip" "$zip_path"
+ fail "could not stage the previous checksum; previous outputs were restored"
+ fi
+fi
+
+if ! mv -- "$staged_zip" "$zip_path"; then
+ [[ ! -f "$previous_zip" ]] || mv -- "$previous_zip" "$zip_path"
+ [[ ! -f "$previous_checksum" ]] || mv -- "$previous_checksum" "$checksum_path"
+ fail "could not publish the verified portable ZIP"
+fi
+if ! mv -- "$staged_checksum" "$checksum_path"; then
+ mv -- "$zip_path" "$staged_zip"
+ [[ ! -f "$previous_zip" ]] || mv -- "$previous_zip" "$zip_path"
+ [[ ! -f "$previous_checksum" ]] || mv -- "$previous_checksum" "$checksum_path"
+ fail "could not publish the portable ZIP checksum; previous outputs were restored"
+fi
+
+echo "Portable macOS package: $zip_path"
+echo "SHA-256: $zip_hash"
+echo "Checksum file: $checksum_path"
From ed0705325c1ee4917d30c8b742db09831f0f5486 Mon Sep 17 00:00:00 2001
From: tslee
Date: Mon, 31 Aug 2026 14:50:27 +0900
Subject: [PATCH 5/6] =?UTF-8?q?=EB=A7=A5=20=ED=8F=AC=ED=84=B0=EB=B8=94=20?=
=?UTF-8?q?=EB=B0=B0=ED=8F=AC=EB=A5=BC=20=EC=95=A0=ED=94=8C=20=EC=8B=A4?=
=?UTF-8?q?=EB=A6=AC=EC=BD=98=20=EC=A0=84=EC=9A=A9=EC=9C=BC=EB=A1=9C=20?=
=?UTF-8?q?=EC=A0=84=ED=99=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/macos-portable.yml | 36 +++-------
.../licenses/THIRD_PARTY_NOTICES_MACOS.md | 8 +--
README.md | 70 ++++++++-----------
docs/AI_WORKFLOW.md | 17 +++--
docs/CHANGELOG.md | 5 +-
docs/DECISIONS.md | 18 ++---
docs/KNOWN_ISSUES.md | 9 ++-
docs/MACOS_GUIDE.md | 30 ++++----
docs/PACKAGE_README_MACOS.md | 54 +++++++-------
docs/PROJECT_BRIEF.md | 10 +--
docs/README.md | 2 +-
docs/SESSION.md | 27 +++----
docs/TODO.md | 12 ++--
scripts/Package-Mac-Release.sh | 12 +---
14 files changed, 134 insertions(+), 176 deletions(-)
diff --git a/.github/workflows/macos-portable.yml b/.github/workflows/macos-portable.yml
index 5401f1b..b501576 100644
--- a/.github/workflows/macos-portable.yml
+++ b/.github/workflows/macos-portable.yml
@@ -1,4 +1,4 @@
-name: macOS portable packages
+name: macOS Apple Silicon portable package
on:
pull_request:
@@ -53,21 +53,8 @@ permissions:
jobs:
build-portable:
- name: ${{ matrix.name }}
- strategy:
- fail-fast: false
- matrix:
- include:
- - name: Apple Silicon arm64
- runner: macos-15
- rid: osx-arm64
- package_arch: arm64
- - name: Intel x64
- runner: macos-15-intel
- rid: osx-x64
- package_arch: x64
-
- runs-on: ${{ matrix.runner }}
+ name: Apple Silicon arm64
+ runs-on: macos-15
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -104,17 +91,17 @@ jobs:
- name: Build and verify portable ZIP
run: >-
scripts/Package-Mac-Release.sh
- --rid "${{ matrix.rid }}"
+ --rid osx-arm64
--version "$DXM_PACKAGE_VERSION"
--skip-tests
- name: Upload prebuilt portable package
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
- name: DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-${{ matrix.package_arch }}
+ name: DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-arm64
path: |
- dist/DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-${{ matrix.package_arch }}.zip
- dist/DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-${{ matrix.package_arch }}.zip.sha256
+ dist/DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-arm64.zip
+ dist/DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-arm64.zip.sha256
if-no-files-found: error
compression-level: 0
retention-days: 30
@@ -128,7 +115,7 @@ jobs:
permissions:
contents: write
steps:
- - name: Download both verified packages
+ - name: Download the verified package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: DX-Manager-v*-macos-*
@@ -143,12 +130,10 @@ jobs:
expected=(
"DX-Manager-v${version}-macos-arm64.zip"
"DX-Manager-v${version}-macos-arm64.zip.sha256"
- "DX-Manager-v${version}-macos-x64.zip"
- "DX-Manager-v${version}-macos-x64.zip.sha256"
)
mapfile -t actual < <(find release-assets -type f -print | sort)
if [[ "${#actual[@]}" -ne "${#expected[@]}" ]]; then
- echo "Expected exactly four release files, found ${#actual[@]}." >&2
+ echo "Expected exactly two release files, found ${#actual[@]}." >&2
exit 1
fi
for filename in "${expected[@]}"; do
@@ -160,10 +145,9 @@ jobs:
(
cd release-assets
sha256sum --check "DX-Manager-v${version}-macos-arm64.zip.sha256"
- sha256sum --check "DX-Manager-v${version}-macos-x64.zip.sha256"
)
- - name: Create draft release with verified packages
+ - name: Create draft release with verified package
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
diff --git a/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md b/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md
index f96cd07..640bf17 100644
--- a/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md
+++ b/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md
@@ -1,7 +1,7 @@
# DX Manager for macOS - Third-Party Notices
-DX Manager is an independently developed utility. The macOS portable packages
-include the components listed below from official project sources or archives.
+DX Manager is an independently developed utility. The macOS portable package
+includes the components listed below from official project sources or archives.
Packaging applies ad-hoc macOS signatures to the DX Manager executables and to
a third-party executable only when it does not already have a valid signature.
This changes signing metadata, not program code. Those components remain
@@ -20,8 +20,8 @@ Official source: https://github.com/dotnet/runtime/tree/v8.0.30
## scrcpy
-The package includes the official static scrcpy 4.1 archive for the selected
-Mac processor architecture. scrcpy and scrcpy-server are licensed under the
+The package includes the official static scrcpy 4.1 archive for Apple Silicon
+arm64. scrcpy and scrcpy-server are licensed under the
Apache License, Version 2.0. The complete license supplied by the official
archive is retained in `tools/scrcpy/LICENSE` and is also provided as
`scrcpy-LICENSE.txt` in the package license directory.
diff --git a/README.md b/README.md
index 8b80a47..cfde534 100644
--- a/README.md
+++ b/README.md
@@ -91,10 +91,9 @@ distribution.
recovery, phone-to-PC file transfer, a Quick Settings tile, and a compact
home-screen widget
- Dual OS support: Dedicated Windows WinForms GUI and native macOS (.NET 8 TUI & CLI host)
-- 64-bit Windows 7 SP1 to Windows 11 compatibility (.NET 4.6.2), plus macOS 14+
- portable targets for Apple Silicon and Intel x86_64. The Intel package has
- passed Rosetta executable checks; a physical Intel Mac DeX session is not yet
- verified.
+- 64-bit Windows 7 SP1 to Windows 11 compatibility (.NET 4.6.2), plus a macOS
+ 14+ portable target for Apple Silicon (Apple M-series, arm64). Intel Macs
+ (x86_64) are not supported, and no x64 package is provided.
## Design Philosophy
@@ -321,24 +320,23 @@ scripts/Package-Release.ps1
See [DexManager/README.md](DexManager/README.md) for packaging notes.
### 2. macOS Edition (Cross-Platform Host & TUI Dashboard)
-- **Target**: macOS 14 Sonoma or later (Apple Silicon & Intel x86_64)
-- **Portable packages**: `DX-Manager-v2.0.0-macos-arm64.zip` and
- `DX-Manager-v2.0.0-macos-x64.zip`
+- **Target**: macOS 14 Sonoma or later on Apple Silicon (Apple M-series, arm64).
+ Intel Macs (x86_64) are not supported.
+- **Portable package**: `DX-Manager-v2.0.0-macos-arm64.zip`
- **End-user prerequisites**: no Homebrew and no separate .NET installation;
- the packages include a self-contained .NET runtime, scrcpy 4.1, ADB, and the
+ the package includes a self-contained .NET runtime, scrcpy 4.1, ADB, and the
scrcpy server
- **Developer toolchain**: pinned .NET 8 SDK (`global.json`)
- **Solution**: `DexManager.Mac.sln`
-- **Developer packaging output**: `dist/DX-Manager-v2.0.0-macos-*.zip`
+- **Developer packaging output**: `dist/DX-Manager-v2.0.0-macos-arm64.zip`
- **Features**: Interactive terminal UI (TUI) dashboard, CLI argument mode (`--dex`, `--diag`), native macOS path resolution, and multi-device support.
-Users download the package matching their Mac, extract the complete ZIP, and
-double-click `Start DX Manager.command`. They do not build the source. A
-version tag such as `v2.0.0` is configured to trigger fresh Apple Silicon and
-Intel builds. When both jobs succeed, the workflow creates a draft GitHub
-Release containing both verified ZIPs and their SHA-256 files. A maintainer
-reviews the draft before publishing it. The first remote workflow run for this
-change still needs to be confirmed.
+Users download the arm64 package, extract the complete ZIP, and double-click
+`Start DX Manager.command`. They do not build the source. A version tag such as
+`v2.0.0` is configured to trigger a fresh Apple Silicon build. When the job
+succeeds, the workflow creates a draft GitHub Release containing the verified
+ZIP and its SHA-256 file. A maintainer reviews the draft before publishing it.
+The first remote workflow run for this change still needs to be confirmed.
The following commands are for maintainers who need to reproduce the packages
locally:
@@ -346,14 +344,11 @@ locally:
```bash
# Build and verify a prebuilt Apple Silicon portable ZIP
scripts/Package-Mac-Release.sh --rid osx-arm64
-
-# Build and verify a prebuilt Intel portable ZIP
-scripts/Package-Mac-Release.sh --rid osx-x64
```
The repository workflow is configured to run the build and test suites on
-matching Apple Silicon and Intel runners. Pull requests retain both packages as
-workflow artifacts; version tags create a draft Release with the same verified
-files. See the [portable package guide](docs/PACKAGE_README_MACOS.md)
+an Apple Silicon runner. Pull requests retain the package as a workflow
+artifact; version tags create a draft Release with the same verified files.
+See the [portable package guide](docs/PACKAGE_README_MACOS.md)
and the detailed [macOS guide](docs/MACOS_GUIDE.md).
## Project Status
@@ -475,8 +470,8 @@ DX Manager는 개인적으로 사용하던 Batch 스크립트, CMD 명령과 Aut
빠른 설정 타일과 2 × 1 홈 위젯을 제공하는 공식 검증형 DX Companion 번들
- Windows 및 macOS 2가지 플랫폼 전용 빌드 지원 (Windows WinForms GUI & macOS .NET 8 TUI 대시보드/CLI)
- 64비트 Windows 7 SP1~11 (.NET 4.6.2) 호환 및 macOS 14 Sonoma 이상
- Apple Silicon/Intel용 포터블 패키지 제공 목표. Intel 패키지는 Rosetta 기동을
- 확인했으며 실제 Intel Mac의 DeX 실기는 아직 확인하지 않았습니다.
+ Apple Silicon(Apple M 시리즈, arm64)용 포터블 패키지 제공. Intel
+ Mac(x86_64)은 지원하지 않으며 x64 패키지를 제공하지 않습니다.
## 개발 철학
@@ -696,35 +691,32 @@ scripts/Package-Release.ps1
배포 파일 구성은 [DexManager/README.md](DexManager/README.md)를 참조하십시오.
### 2. macOS 에디션 (크로스플랫폼 호스트 & TUI 대시보드)
-- **지원 환경**: macOS 14 Sonoma 이상 (Apple Silicon 및 Intel x86_64)
-- **포터블 패키지**: `DX-Manager-v2.0.0-macos-arm64.zip` 및
- `DX-Manager-v2.0.0-macos-x64.zip`
+- **지원 환경**: macOS 14 Sonoma 이상 Apple Silicon(Apple M 시리즈, arm64).
+ Intel Mac(x86_64)은 지원하지 않습니다.
+- **포터블 패키지**: `DX-Manager-v2.0.0-macos-arm64.zip`
- **사용자 사전 설치**: Homebrew와 별도 .NET 설치 불필요. self-contained
.NET 런타임, scrcpy 4.1, ADB와 scrcpy 서버를 ZIP에 포함
- **개발 환경**: `global.json`에 고정된 .NET 8 SDK
- **솔루션**: `DexManager.Mac.sln`
-- **개발자용 패키징 산출물**: `dist/DX-Manager-v2.0.0-macos-*.zip`
+- **개발자용 패키징 산출물**: `dist/DX-Manager-v2.0.0-macos-arm64.zip`
- **특징**: ANSI 대화형 콘솔 대시보드(TUI), CLI 인자 실행 모드(`--dex`, `--diag`), macOS 표준 경로 및 다중 기기 독립 세션 제어.
-일반 사용자는 Mac에 맞는 패키지를 내려받아 ZIP 전체의 압축을 풀고
+일반 사용자는 arm64 패키지를 내려받아 ZIP 전체의 압축을 풀고
`Start DX Manager.command`를 더블클릭합니다. 소스 빌드는 필요하지 않습니다.
-`v2.0.0` 같은 버전 태그를 push하면 Apple Silicon용과 Intel용 패키지를 새로
-빌드하도록 구성했습니다. 두 작업이 성공하면 검증된 ZIP 두 개와 SHA-256 파일이
-포함된 GitHub Release 초안을 만들며, 유지관리자가 확인한 뒤 공개합니다. 이 변경의
-첫 원격 workflow 성공 여부는 아직 확인해야 합니다.
+`v2.0.0` 같은 버전 태그를 push하면 Apple Silicon용 패키지를 새로 빌드하도록
+구성했습니다. 작업이 성공하면 검증된 ZIP과 SHA-256 파일이 포함된 GitHub Release
+초안을 만들며, 유지관리자가 확인한 뒤 공개합니다. 이 변경의 첫 원격 workflow 성공
+여부는 아직 확인해야 합니다.
다음 명령은 패키지를 로컬에서 재현하려는 유지관리자용입니다.
```bash
# Apple Silicon 포터블 ZIP 미리 빌드 및 검증
scripts/Package-Mac-Release.sh --rid osx-arm64
-
-# Intel 포터블 ZIP 미리 빌드 및 검증
-scripts/Package-Mac-Release.sh --rid osx-x64
```
-저장소의 GitHub Actions workflow는 Apple Silicon과 Intel 실행 환경에서 각각
-빌드·테스트하도록 구성되어 있습니다. PR에서는 두 패키지를 workflow artifact로
-보관하고, 버전 태그에서는 같은 검증 파일을 넣은 Release 초안을 만듭니다.
+저장소의 GitHub Actions workflow는 Apple Silicon 실행 환경에서 빌드·테스트하도록
+구성되어 있습니다. PR에서는 arm64 패키지를 workflow artifact로 보관하고, 버전
+태그에서는 같은 검증 파일을 넣은 Release 초안을 만듭니다.
[포터블 패키지 안내](docs/PACKAGE_README_MACOS.md)와 [macOS
가이드](docs/MACOS_GUIDE.md)를 참조하십시오.
diff --git a/docs/AI_WORKFLOW.md b/docs/AI_WORKFLOW.md
index 7715455..7246a83 100644
--- a/docs/AI_WORKFLOW.md
+++ b/docs/AI_WORKFLOW.md
@@ -48,23 +48,22 @@
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\Package-Release.ps1
```
-macOS 공개 후보는 사용자가 빌드하지 않아도 되도록 아키텍처별 self-contained
-ZIP을 미리 만든다.
+macOS 공개 후보는 사용자가 빌드하지 않아도 되도록 Apple Silicon arm64
+self-contained ZIP을 미리 만든다. Intel Mac용 x64 ZIP은 만들지 않는다.
```bash
scripts/Package-Mac-Release.sh --rid osx-arm64
-scripts/Package-Mac-Release.sh --rid osx-x64
```
-macOS 스크립트는 현재 저장소의 x86 전용 개발 도구를 그대로 복사하지 않는다.
-DX Manager와 `DXMAdbProxy`를 대상 RID로 publish하고, SHA-256을 고정한 공식
-scrcpy 4.1 macOS 정적 아카이브를 아키텍처별로 넣는다. ZIP을 다시 풀어 실행
+macOS 스크립트는 현재 저장소의 다른 아키텍처용 개발 도구를 그대로 복사하지
+않는다. DX Manager와 `DXMAdbProxy`를 `osx-arm64` RID로 publish하고, SHA-256을
+고정한 공식 scrcpy 4.1 macOS arm64 정적 아카이브를 넣는다. ZIP을 다시 풀어 실행
권한, Mach-O 아키텍처, 외부 Homebrew·사용자 경로 의존성, 버전, 필수 문서와
사용자 데이터·디버그 파일 제외를 확인한다. 파일 권한·순서·시각은
`SOURCE_DATE_EPOCH` 또는 현재 Git 커밋 시각으로 정규화한다. GitHub Actions
-workflow는 `macos-15`와 `macos-15-intel`에서 두 패키지를 각각 빌드·테스트해
-PR artifact로 보관한다. `v*` 버전 태그에서는 두 ZIP과 체크섬을 다시 검증한 뒤
-Release 초안을 만들고, 유지관리자가 초안을 확인한 뒤 공개한다.
+workflow는 Apple Silicon `macos-15`에서 arm64 패키지를 빌드·테스트해 PR
+artifact로 보관한다. `v*` 버전 태그에서는 ZIP과 체크섬을 다시 검증한 뒤 Release
+초안을 만들고, 유지관리자가 초안을 확인한 뒤 공개한다.
시스템에 .NET Framework 4.6.2 Developer Pack이 없으면 참조 어셈블리 루트를
`-TargetFrameworkRootPath`로 지정하거나 현재 셸의
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 8c60374..5889dd1 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -4,8 +4,9 @@
## 2026-08 - v2.0.0 (macOS Cross-Platform Edition)
-- Apple Silicon arm64와 Intel x64용 self-contained 포터블 ZIP 패키징 추가
-- GitHub Actions가 각 Mac 아키텍처에서 단위·통합 테스트와 다중 기기 테스트를
+- Apple Silicon arm64용 self-contained 포터블 ZIP 패키징 추가. Intel Mac은
+ 지원하지 않으며 x64 ZIP을 제공하지 않음
+- GitHub Actions가 Apple Silicon에서 단위·통합 테스트와 다중 기기 테스트를
실행한 뒤 미리 빌드된 ZIP과 SHA-256 파일을 artifact로 업로드하고, 버전
태그에서는 정확한 파일명과 체크섬을 확인해 Release 초안을 만들도록 구성
- 공식 scrcpy 4.1 macOS 정적 아카이브의 SHA-256, Mach-O 아키텍처, 실행 권한,
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
index 1be9145..ac2e6c5 100644
--- a/docs/DECISIONS.md
+++ b/docs/DECISIONS.md
@@ -327,13 +327,14 @@ Scrcpy, DeX orchestrator, 단일창, 화면 OFF, Companion 수신기와 PC→휴
보존하는 것이 우선이다. 공통화는 이 경로를 직접 검증하는 테스트가 생긴 뒤 별도
작업으로 수행한다.
-## 2026-08 - macOS는 아키텍처별 self-contained ZIP으로 배포한다
+## 2026-08 - macOS는 Apple Silicon arm64 self-contained ZIP으로 배포한다
macOS 사용자가 저장소를 clone하거나 .NET, Homebrew, scrcpy와 ADB를 직접
설치하도록 하지 않는다. 원본 Windows 프로젝트가 실행 파일과 도구 폴더 전체를
-미리 만든 ZIP으로 배포하는 것과 같은 원칙을 적용한다. Apple Silicon arm64와
-Intel x64는 하나의 universal ZIP으로 합치지 않고 각각 해당 RID로 DX Manager와
-ADB proxy를 self-contained publish한다.
+미리 만든 ZIP으로 배포하는 것과 같은 원칙을 적용한다. 공식 지원과 검증 범위는
+Apple Silicon arm64로 한정하고 `osx-arm64` RID로 DX Manager와 ADB proxy를
+self-contained publish한다. Intel Mac(x86_64)은 지원하지 않으며 universal 또는
+x64 ZIP을 만들지 않는다.
.NET single-file 내부 압축은 사용하지 않는다. macOS arm64 self-contained
실행 파일을 같은 추출 캐시에서 연속 실행할 때 내부 압축을 켠 빌드에서
@@ -341,11 +342,10 @@ ADB proxy를 self-contained publish한다.
연속 실행을 통과했다. 실행 파일은 계속 single-file로 publish하며 최종 ZIP의
표준 압축은 유지한다.
-저장소의 개발용 `tools`에는 x86 전용 파일이 있을 수 있으므로 공개 패키징에서
-그 폴더를 무조건 복사하지 않는다. 공식 scrcpy 4.1 macOS 정적 아카이브를
-아키텍처별 URL과 SHA-256으로 고정해 넣고, 번들 도구를 PATH와 Homebrew보다 먼저
-사용한다. GitHub Actions도 실제 아키텍처가 일치하는 `macos-15`와
-`macos-15-intel`에서 별도로 빌드·실행 검증한다.
+저장소의 개발용 `tools`에는 다른 아키텍처의 파일이 있을 수 있으므로 공개
+패키징에서 그 폴더를 무조건 복사하지 않는다. 공식 scrcpy 4.1 macOS arm64 정적
+아카이브의 URL과 SHA-256을 고정해 넣고, 번들 도구를 PATH와 Homebrew보다 먼저
+사용한다. GitHub Actions는 Apple Silicon `macos-15`에서 빌드·실행 검증한다.
DeX overlay를 만들거나 제거하기 직전에는 선택 화면에 남은 identity만 신뢰하지
않고 해당 ADB transport에서 안정적인 물리 identity를 다시 읽는다. 이미 알고 있던
diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md
index e84448d..24b1690 100644
--- a/docs/KNOWN_ISSUES.md
+++ b/docs/KNOWN_ISSUES.md
@@ -143,14 +143,13 @@ DX Manager가 만든 화면과 사용자가 개발자 옵션에서 직접 선택
## macOS 포터블 서명과 실기 범위
-macOS arm64/x64 ZIP은 self-contained이므로 Homebrew와 별도 .NET 설치에
-의존하지 않는다. 다만 현재 자동화 설정에는 Apple Developer ID 인증서와
+macOS arm64 ZIP은 self-contained이므로 Homebrew와 별도 .NET 설치에 의존하지
+않는다. Intel Mac(x86_64)은 지원 범위가 아니며 x64 ZIP을 제공하지 않는다. 다만
+현재 자동화 설정에는 Apple Developer ID 인증서와
notarization 자격 증명이 구성되어 있지 않다. 브라우저로 받은 ZIP은 최초 실행
시 Gatekeeper 승인이 필요할 수 있으며, 서명·공증 전에는 모든 macOS
버전에서 경고 없는 최초 실행을 보장하지 않는다. 패키징 스크립트가 quarantine
속성을 자동 삭제하거나 macOS 보안 기능을 우회하지 않는다.
Apple Silicon ZIP은 Apple Silicon Mac에서 실제 바이너리 기동을 확인했다.
-Intel ZIP은 아키텍처와 외부 경로를 검사하고 Apple Silicon Mac의 Rosetta에서
-실행 파일 기동을 확인했지만, Intel 실기 전체 DeX 흐름은 GitHub의
-`macos-15-intel` 자동 빌드와 별도로 실제 Intel Mac 확인이 남아 있다.
+Intel Mac용 빌드·배포·실기 검증은 수행하지 않는다.
diff --git a/docs/MACOS_GUIDE.md b/docs/MACOS_GUIDE.md
index c86266b..c7cca00 100644
--- a/docs/MACOS_GUIDE.md
+++ b/docs/MACOS_GUIDE.md
@@ -17,12 +17,11 @@
## 2. 시스템 요구사항 (Requirements)
- **운영체제**: macOS 14 Sonoma 이상
-- **아키텍처**:
- - Apple Silicon (Apple M series) - Native ARM64
- - Intel Mac (x86_64)
+- **아키텍처**: Apple Silicon(Apple M 시리즈, arm64) 전용
+- **미지원**: Intel Mac(x86_64)은 지원하지 않으며 x64 ZIP을 제공하지 않습니다.
- **포터블 ZIP 사용자**: Homebrew, .NET, scrcpy와 ADB를 별도로 설치하지
- 않습니다. Mac 아키텍처에 맞게 미리 빌드된 ZIP에 self-contained .NET
- 런타임, scrcpy 4.1, ADB와 scrcpy 서버가 포함됩니다.
+ 않습니다. Apple Silicon용으로 미리 빌드된 ZIP에 self-contained .NET 런타임,
+ scrcpy 4.1, ADB와 scrcpy 서버가 포함됩니다.
- **소스 개발자**: `global.json`에 지정된 .NET 8 SDK가 필요합니다.
- **지원 스마트폰**:
- Samsung Galaxy 기기 중 Samsung DeX를 지원하는 기기 (Galaxy S시리즈, Note시리즈, Z Fold시리즈, Tab S시리즈 등)
@@ -32,20 +31,18 @@
## 3. 포터블 ZIP 실행 (Portable Release)
-### 3.1 Mac에 맞는 ZIP 선택
+### 3.1 지원 ZIP 확인
- Apple M 시리즈 Mac: `DX-Manager-v-macos-arm64.zip`
-- Intel Mac: `DX-Manager-v-macos-x64.zip`
여기서 ``은 공개된 Release 버전(예: `2.0.0`)으로 바꿉니다. ZIP 안의
문서에는 패키징 시 실제 버전이 자동 반영됩니다.
-저장소의 GitHub Actions workflow는 `macos-15` Apple Silicon 실행 환경과
-`macos-15-intel` Intel 실행 환경에서 각각 전체 빌드·테스트·패키지 검증을
-수행하도록 구성되어 있습니다. 버전 태그의 두 작업이 성공하면 검증된 ZIP 두 개와
-SHA-256 파일을 포함한 GitHub Release 초안을 만들며, 유지관리자가 확인 후
-공개합니다. 이 변경의 첫 원격 workflow 성공 여부는 아직 확인해야 합니다.
-Release가 공개된 뒤 사용자는 Mac에 맞는 ZIP 하나를
+저장소의 GitHub Actions workflow는 `macos-15` Apple Silicon 실행 환경에서 전체
+빌드·테스트·패키지 검증을 수행하도록 구성되어 있습니다. 버전 태그 작업이 성공하면
+검증된 arm64 ZIP과 SHA-256 파일을 포함한 GitHub Release 초안을 만들며,
+유지관리자가 확인 후 공개합니다. 이 변경의 첫 원격 workflow 성공 여부는 아직
+확인해야 합니다. Release가 공개된 뒤 사용자는 arm64 ZIP을
내려받아 전체 폴더의 압축을 풀고 `Start DX Manager.command`를 더블클릭합니다.
소스 빌드는 필요하지 않습니다.
@@ -92,17 +89,14 @@ dotnet test DexManager.Mac.sln -c Release
dotnet run --project DexManager.MultiDeviceTests -c Release
```
-### 4.3 아키텍처별 포터블 ZIP 생성
+### 4.3 Apple Silicon 포터블 ZIP 생성
```bash
# Apple Silicon용 self-contained ZIP
scripts/Package-Mac-Release.sh --rid osx-arm64
-
-# Intel용 self-contained ZIP
-scripts/Package-Mac-Release.sh --rid osx-x64
```
-스크립트는 DX Manager와 ADB proxy를 지정한 RID로 미리 publish하고, 공식
+스크립트는 DX Manager와 ADB proxy를 `osx-arm64` RID로 미리 publish하고, 공식
scrcpy 4.1 정적 빌드의 SHA-256을 확인한 뒤 번들합니다. 생성한 ZIP을 새 임시
폴더에 다시 풀어 실행 권한, CPU 아키텍처, 외부 Homebrew 경로 의존성, 버전,
라이선스와 사용자 데이터 제외 여부를 검사합니다.
diff --git a/docs/PACKAGE_README_MACOS.md b/docs/PACKAGE_README_MACOS.md
index d21d7f7..a0f87f4 100644
--- a/docs/PACKAGE_README_MACOS.md
+++ b/docs/PACKAGE_README_MACOS.md
@@ -5,21 +5,19 @@ complete archive and run it from the extracted folder. The package includes the
DX Manager runtime, scrcpy 4.1, ADB, and the scrcpy server. Homebrew and a
separate .NET installation are not required.
-The portable packages require **macOS 14 Sonoma or later**.
+The portable package requires **macOS 14 Sonoma or later** and an Apple
+Silicon Mac.
## English
-### 1. Choose the correct package
+### 1. Confirm that the Mac is supported
-- **Apple Silicon Mac:** choose the package labeled **arm64**. This applies to
- Macs with an Apple M-series chip.
-- **Intel Mac:** choose the package labeled **x64** or **x86_64**.
+- **Supported:** Apple Silicon Macs with an Apple M-series chip. Use the package
+ labeled **arm64**.
+- **Not supported:** Intel Macs (x86_64). An x64 package is not provided.
If you are not sure which Mac you have, open the Apple menu and select **About
-This Mac**. The window shows either an Apple chip or an Intel processor.
-
-The two packages provide the same DX Manager functions. Only their processor
-architecture is different.
+This Mac**. Continue only when the window shows an Apple chip.
### 2. Verify the downloaded ZIP
@@ -34,9 +32,8 @@ shasum -a 256 -c "DX-Manager-v${release_version}-macos-arm64.zip.sha256"
When reading this source document on GitHub, replace `` with the
Release version shown in the downloaded filename, for example `2.0.0`. The
README inside a packaged ZIP already contains the exact package version.
-For an Intel Mac, replace `arm64` with `x64`. Continue only when the command
-prints `OK`. If it reports a mismatch, do not run the ZIP; download both files
-again from the Release.
+Continue only when the command prints `OK`. If it reports a mismatch, do not
+run the ZIP; download both files again from the Release.
### 3. Extract and keep the complete folder
@@ -98,15 +95,14 @@ on every supported Mac and macOS version cannot be guaranteed.
### Package contents and limits
-- Bundled DX Manager runtime for the selected Mac architecture
+- Bundled DX Manager runtime for Apple Silicon arm64
- Bundled scrcpy 4.1, ADB, and scrcpy server
- No Homebrew requirement
- No separate .NET installation requirement
- No DX Companion APK in the current macOS package
-The Intel executable has been checked under Rosetta on Apple Silicon, while a
-complete Galaxy DeX session on physical Intel Mac hardware still needs direct
-verification.
+Intel Macs are outside the supported scope, and no Intel x64 executable is
+included in the release.
Do not add, replace, or delete bundled ADB and scrcpy files unless the package
instructions for a later version explicitly require it.
@@ -120,18 +116,17 @@ DX Manager macOS 버전은 포터블 ZIP 패키지로 배포됩니다. ZIP 전
ADB와 scrcpy 서버가 포함됩니다. Homebrew와 별도의 .NET 설치는 필요하지
않습니다.
-포터블 패키지의 최소 운영체제는 **macOS 14 Sonoma**입니다.
+포터블 패키지는 **macOS 14 Sonoma 이상 Apple Silicon Mac**에서 사용할 수
+있습니다.
-### 1. Mac에 맞는 패키지 선택
+### 1. 지원 대상 Mac 확인
-- **Apple Silicon Mac:** **arm64**로 표시된 패키지를 선택하십시오. Apple M
- 시리즈 칩이 탑재된 Mac이 여기에 해당합니다.
-- **Intel Mac:** **x64** 또는 **x86_64**로 표시된 패키지를 선택하십시오.
+- **지원:** Apple M 시리즈 칩이 탑재된 Apple Silicon Mac. **arm64**로 표시된
+ 패키지를 사용하십시오.
+- **미지원:** Intel Mac(x86_64). x64 패키지는 제공하지 않습니다.
Mac의 종류를 모르면 Apple 메뉴에서 **이 Mac에 관하여**를 여십시오. 이 화면에
-Apple 칩 또는 Intel 프로세서가 표시됩니다.
-
-두 패키지의 DX Manager 기능은 같습니다. 프로세서 아키텍처만 다릅니다.
+Apple 칩이 표시되는 경우에만 계속하십시오.
### 2. 다운로드한 ZIP 확인
@@ -147,9 +142,8 @@ shasum -a 256 -c "DX-Manager-v${release_version}-macos-arm64.zip.sha256"
이 원본 문서를 GitHub에서 읽는 경우 ``을 내려받은 파일명에 표시된
Release 버전(예: `2.0.0`)으로 바꾸십시오. 패키지 ZIP 안의 README에는 실제
패키지 버전이 이미 반영됩니다.
-Intel Mac에서는 `arm64`를 `x64`로 바꿉니다. 결과에 `OK`가 표시된 경우에만
-계속하십시오. 불일치가 표시되면 실행하지 말고 Release에서 두 파일을 다시
-내려받으십시오.
+결과에 `OK`가 표시된 경우에만 계속하십시오. 불일치가 표시되면 실행하지 말고
+Release에서 두 파일을 다시 내려받으십시오.
### 3. ZIP 전체 압축 해제 및 폴더 유지
@@ -210,14 +204,14 @@ macOS 버전에 따라 메뉴 문구와 위치가 다를 수 있습니다. Apple
### 패키지 구성 및 범위
-- 선택한 Mac 아키텍처용 DX Manager 런타임
+- Apple Silicon arm64용 DX Manager 런타임
- 번들 scrcpy 4.1, ADB와 scrcpy 서버
- Homebrew 설치 불필요
- 별도 .NET 설치 불필요
- 현재 macOS 패키지에는 DX Companion APK가 포함되지 않음
-Intel 실행 파일은 Apple Silicon Mac의 Rosetta에서 기동을 확인했습니다. 실제
-Intel Mac에서 Galaxy DeX 전체 흐름을 확인하는 실기 검증은 남아 있습니다.
+Intel Mac은 지원 범위에 포함하지 않으며, Release에 Intel x64 실행 파일을
+포함하지 않습니다.
이후 버전의 패키지 안내에서 명시적으로 요구하지 않는 한 번들 ADB와 scrcpy
파일을 추가, 교체 또는 삭제하지 마십시오.
diff --git a/docs/PROJECT_BRIEF.md b/docs/PROJECT_BRIEF.md
index 207a92c..a1c0251 100644
--- a/docs/PROJECT_BRIEF.md
+++ b/docs/PROJECT_BRIEF.md
@@ -28,11 +28,11 @@ GitHub 메인 README와 별도로 HTML이 없는 `docs\PACKAGE_README.md`를 배
폴더의 `README.md`로 사용하며, 모든 이중 언어 문서는 영어 다음 한국어
순서로 작성한다.
-macOS 에디션은 `scripts/Package-Mac-Release.sh`로 Apple Silicon arm64와
-Intel x64 self-contained ZIP을 별도로 만든다. 각 ZIP에는 해당 아키텍처로
-미리 publish한 DX Manager와 ADB proxy, 공식 scrcpy 4.1 정적 빌드, ADB,
-scrcpy-server, 실행기와 라이선스를 포함한다. 사용자는 Homebrew나 .NET을
-설치하거나 소스를 빌드하지 않고 ZIP 전체를 풀어 실행한다.
+macOS 에디션은 `scripts/Package-Mac-Release.sh`로 Apple Silicon arm64
+self-contained ZIP을 만든다. ZIP에는 arm64로 미리 publish한 DX Manager와 ADB
+proxy, 공식 scrcpy 4.1 정적 빌드, ADB, scrcpy-server, 실행기와 라이선스를
+포함한다. 사용자는 Homebrew나 .NET을 설치하거나 소스를 빌드하지 않고 ZIP 전체를
+풀어 실행한다. Intel Mac(x86_64)은 지원하지 않으며 x64 ZIP을 만들지 않는다.
## 완료 기능
diff --git a/docs/README.md b/docs/README.md
index c6f1267..f41debe 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -19,7 +19,7 @@
- `RELEASE_NOTES_v2.0.0.md`: v2.0.0 GitHub Release용 영어·한국어 설명
- `RELEASE_NOTES_v1.3.0.md`: 이전 v1.3.0 GitHub Release 기록
- `PACKAGE_README.md`: 배포 ZIP 루트에 들어가는 HTML 없는 영어·한국어 안내
-- `PACKAGE_README_MACOS.md`: macOS arm64/x64 포터블 ZIP 전용 영어·한국어 안내
+- `PACKAGE_README_MACOS.md`: macOS Apple Silicon arm64 포터블 ZIP 전용 영어·한국어 안내
- `../DXDisplayCleanup`: 번들 선택형 Android 복구·파일 전송 앱 소스와 빌드 문서
사용자용 README는 저장소 루트의 `README.md`, 영어/한국어 설명서는
diff --git a/docs/SESSION.md b/docs/SESSION.md
index 4c4bb99..b2ff266 100644
--- a/docs/SESSION.md
+++ b/docs/SESSION.md
@@ -5,19 +5,20 @@
## macOS 포터블 배포 작업
- 작업 브랜치: `codex/macos-portable-release`
-- `scripts/Package-Mac-Release.sh`가 `osx-arm64`와 `osx-x64`용 DX Manager와
- ADB proxy를 self-contained single-file로 publish한다.
-- 공식 scrcpy 4.1 arm64/x86_64 정적 아카이브를 고정 SHA-256으로 검증해
- ADB와 scrcpy-server를 함께 포함한다.
-- `DX-Manager-v2.0.0-macos-arm64.zip`과
- `DX-Manager-v2.0.0-macos-x64.zip`, 각 `.sha256` 파일을 생성한다.
-- GitHub Actions workflow는 `macos-15`와 `macos-15-intel`에서 빌드·95개
- 테스트·39개 다중 기기 테스트·package smoke test를 각각 수행하도록
- 구성했다. PR에서는 artifact를 보관하고, `v*` 태그에서는 정확한 네 파일과
- 체크섬을 검증한 뒤 Release 초안을 만든다. 첫 원격 run 결과 확인은 남아 있다.
-- Apple Silicon ZIP은 native 실행 검증을 통과했다. Intel ZIP은 Mach-O x64
- 검사와 이 Mac의 Rosetta에서 DX Manager, proxy, scrcpy 4.1, ADB 37.0.0
- 실행을 확인했다. 실제 Intel Mac의 Galaxy DeX 실기는 남아 있다.
+- 원본 저장소의 Draft PR #3에서 검토 중이며 제목과 본문은 한국어로 작성한다.
+ Apple Silicon arm64 원격 검증을 마칠 때까지 기존 PR #2에는 댓글이나 상태 변경을
+ 하지 않는다.
+- `scripts/Package-Mac-Release.sh`가 `osx-arm64`용 DX Manager와 ADB proxy를
+ self-contained single-file로 publish한다.
+- 공식 scrcpy 4.1 arm64 정적 아카이브를 고정 SHA-256으로 검증해 ADB와
+ scrcpy-server를 함께 포함한다.
+- `DX-Manager-v2.0.0-macos-arm64.zip`과 해당 `.sha256` 파일을 생성한다.
+- GitHub Actions workflow는 Apple Silicon `macos-15`에서 빌드·95개 테스트·39개
+ 다중 기기 테스트·package smoke test를 수행하도록 구성했다. PR에서는 arm64
+ artifact를 보관하고, `v*` 태그에서는 정확한 ZIP과 체크섬 두 파일을 검증한 뒤
+ Release 초안을 만든다. 첫 원격 run 결과 확인은 남아 있다.
+- Apple Silicon ZIP은 native 실행 검증을 통과했다. Intel Mac(x86_64)은 공식
+ 지원 범위에서 제외했고 x64 패키지를 더 이상 빌드·배포하지 않는다.
- macOS `Q` 종료가 no-op이던 조건을 제거하고 실행 중인 서비스 정지와 DeX
overlay cleanup 완료를 기다리도록 수정했다. CLI `--dex`/`--stop-dex`도 기기
감시가 첫 snapshot을 읽은 뒤 동작하며 시작·자연 종료 대기의 `Ctrl+C`를 실제
diff --git a/docs/TODO.md b/docs/TODO.md
index 247cd06..a268d0a 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -2,16 +2,16 @@
## 다음 작업
-- [x] macOS 아키텍처별 포터블 배포 기반
- - [x] arm64/x64 DX Manager와 ADB proxy self-contained single-file publish
- - [x] 공식 scrcpy 4.1 arm64/x86_64 아카이브 SHA-256 검증 및 번들
+- [x] macOS Apple Silicon 포터블 배포 기반
+ - [x] arm64 DX Manager와 ADB proxy self-contained single-file publish
+ - [x] 공식 scrcpy 4.1 arm64 아카이브 SHA-256 검증 및 번들
- [x] ZIP 재압축 해제, 실행 권한, 아키텍처, 외부 경로, 제외 파일 검증
- - [x] Apple Silicon/x64 GitHub Actions matrix, PR artifact와 검증된 Release 초안
- - [ ] PR에서 첫 GitHub Actions arm64/x64 원격 실행 결과 확인
+ - [x] Apple Silicon GitHub Actions, PR artifact와 검증된 Release 초안
+ - [ ] PR에서 첫 GitHub Actions arm64 원격 실행 결과 확인
- [x] `Q` 정상 종료의 DeX·단일창·전송·overlay cleanup 보강
- [x] DeX 시작·정리 전 live identity 재검증과 identity별 보류 정리 분리
- [x] CLI 시작·자연 종료 대기의 `Ctrl+C` 취소 및 종료 경합 보강
- - [ ] 실제 Intel Mac에서 Galaxy DeX 시작·중지와 overlay cleanup 실기 확인
+ - [x] Intel Mac(x86_64) 지원 범위 제외 및 x64 패키지 미제공 명시
- [ ] Apple Developer ID 서명·notarization 자격이 준비되면 공개 ZIP 서명
- [x] v2.0.0 공개 후보 준비
diff --git a/scripts/Package-Mac-Release.sh b/scripts/Package-Mac-Release.sh
index 011ff2c..99ded76 100755
--- a/scripts/Package-Mac-Release.sh
+++ b/scripts/Package-Mac-Release.sh
@@ -23,7 +23,7 @@ usage() {
Create one prebuilt, self-contained DX Manager portable ZIP for macOS.
Usage:
- scripts/Package-Mac-Release.sh --rid osx-arm64|osx-x64 [options]
+ scripts/Package-Mac-Release.sh --rid osx-arm64 [options]
Options:
--version VERSION Package version (default: 2.0.0)
@@ -34,7 +34,7 @@ Options:
Examples:
scripts/Package-Mac-Release.sh --rid osx-arm64
- scripts/Package-Mac-Release.sh --rid osx-x64 --version 2.0.0
+ scripts/Package-Mac-Release.sh --rid osx-arm64 --version 2.0.0
EOF
}
@@ -140,14 +140,8 @@ case "$rid" in
scrcpy_asset_arch="aarch64"
scrcpy_sha256="20fd47c9014dd5e0fa77091f3cb7adbda8445a360c4584aeaa0150b5b3988ff3"
;;
- osx-x64)
- package_arch="x64"
- binary_arch="x86_64"
- scrcpy_asset_arch="x86_64"
- scrcpy_sha256="ee2a7223bc8dbdc4f482db1134bcf441178dafb833492b71ca4c22090c58ce72"
- ;;
*)
- fail "unsupported RID '$rid'; use osx-arm64 or osx-x64"
+ fail "unsupported RID '$rid'; only osx-arm64 is supported"
;;
esac
From ec374a58258ef9889b8b1b2b8a0749d50d75c2c7 Mon Sep 17 00:00:00 2001
From: Kim Dongup
Date: Mon, 31 Aug 2026 12:48:26 -1000
Subject: [PATCH 6/6] =?UTF-8?q?Revert=20"=EB=A7=A5=20=ED=8F=AC=ED=84=B0?=
=?UTF-8?q?=EB=B8=94=20=EB=B0=B0=ED=8F=AC=EB=A5=BC=20=EC=95=A0=ED=94=8C=20?=
=?UTF-8?q?=EC=8B=A4=EB=A6=AC=EC=BD=98=20=EC=A0=84=EC=9A=A9=EC=9C=BC?=
=?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This reverts commit ed0705325c1ee4917d30c8b742db09831f0f5486.
---
.github/workflows/macos-portable.yml | 36 +++++++---
.../licenses/THIRD_PARTY_NOTICES_MACOS.md | 8 +--
README.md | 70 +++++++++++--------
docs/AI_WORKFLOW.md | 17 ++---
docs/CHANGELOG.md | 5 +-
docs/DECISIONS.md | 18 ++---
docs/KNOWN_ISSUES.md | 9 +--
docs/MACOS_GUIDE.md | 30 ++++----
docs/PACKAGE_README_MACOS.md | 54 +++++++-------
docs/PROJECT_BRIEF.md | 10 +--
docs/README.md | 2 +-
docs/SESSION.md | 27 ++++---
docs/TODO.md | 12 ++--
scripts/Package-Mac-Release.sh | 12 +++-
14 files changed, 176 insertions(+), 134 deletions(-)
diff --git a/.github/workflows/macos-portable.yml b/.github/workflows/macos-portable.yml
index b501576..5401f1b 100644
--- a/.github/workflows/macos-portable.yml
+++ b/.github/workflows/macos-portable.yml
@@ -1,4 +1,4 @@
-name: macOS Apple Silicon portable package
+name: macOS portable packages
on:
pull_request:
@@ -53,8 +53,21 @@ permissions:
jobs:
build-portable:
- name: Apple Silicon arm64
- runs-on: macos-15
+ name: ${{ matrix.name }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: Apple Silicon arm64
+ runner: macos-15
+ rid: osx-arm64
+ package_arch: arm64
+ - name: Intel x64
+ runner: macos-15-intel
+ rid: osx-x64
+ package_arch: x64
+
+ runs-on: ${{ matrix.runner }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -91,17 +104,17 @@ jobs:
- name: Build and verify portable ZIP
run: >-
scripts/Package-Mac-Release.sh
- --rid osx-arm64
+ --rid "${{ matrix.rid }}"
--version "$DXM_PACKAGE_VERSION"
--skip-tests
- name: Upload prebuilt portable package
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
- name: DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-arm64
+ name: DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-${{ matrix.package_arch }}
path: |
- dist/DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-arm64.zip
- dist/DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-arm64.zip.sha256
+ dist/DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-${{ matrix.package_arch }}.zip
+ dist/DX-Manager-v${{ env.DXM_PACKAGE_VERSION }}-macos-${{ matrix.package_arch }}.zip.sha256
if-no-files-found: error
compression-level: 0
retention-days: 30
@@ -115,7 +128,7 @@ jobs:
permissions:
contents: write
steps:
- - name: Download the verified package
+ - name: Download both verified packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: DX-Manager-v*-macos-*
@@ -130,10 +143,12 @@ jobs:
expected=(
"DX-Manager-v${version}-macos-arm64.zip"
"DX-Manager-v${version}-macos-arm64.zip.sha256"
+ "DX-Manager-v${version}-macos-x64.zip"
+ "DX-Manager-v${version}-macos-x64.zip.sha256"
)
mapfile -t actual < <(find release-assets -type f -print | sort)
if [[ "${#actual[@]}" -ne "${#expected[@]}" ]]; then
- echo "Expected exactly two release files, found ${#actual[@]}." >&2
+ echo "Expected exactly four release files, found ${#actual[@]}." >&2
exit 1
fi
for filename in "${expected[@]}"; do
@@ -145,9 +160,10 @@ jobs:
(
cd release-assets
sha256sum --check "DX-Manager-v${version}-macos-arm64.zip.sha256"
+ sha256sum --check "DX-Manager-v${version}-macos-x64.zip.sha256"
)
- - name: Create draft release with verified package
+ - name: Create draft release with verified packages
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
diff --git a/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md b/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md
index 640bf17..f96cd07 100644
--- a/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md
+++ b/DexManager/licenses/THIRD_PARTY_NOTICES_MACOS.md
@@ -1,7 +1,7 @@
# DX Manager for macOS - Third-Party Notices
-DX Manager is an independently developed utility. The macOS portable package
-includes the components listed below from official project sources or archives.
+DX Manager is an independently developed utility. The macOS portable packages
+include the components listed below from official project sources or archives.
Packaging applies ad-hoc macOS signatures to the DX Manager executables and to
a third-party executable only when it does not already have a valid signature.
This changes signing metadata, not program code. Those components remain
@@ -20,8 +20,8 @@ Official source: https://github.com/dotnet/runtime/tree/v8.0.30
## scrcpy
-The package includes the official static scrcpy 4.1 archive for Apple Silicon
-arm64. scrcpy and scrcpy-server are licensed under the
+The package includes the official static scrcpy 4.1 archive for the selected
+Mac processor architecture. scrcpy and scrcpy-server are licensed under the
Apache License, Version 2.0. The complete license supplied by the official
archive is retained in `tools/scrcpy/LICENSE` and is also provided as
`scrcpy-LICENSE.txt` in the package license directory.
diff --git a/README.md b/README.md
index cfde534..8b80a47 100644
--- a/README.md
+++ b/README.md
@@ -91,9 +91,10 @@ distribution.
recovery, phone-to-PC file transfer, a Quick Settings tile, and a compact
home-screen widget
- Dual OS support: Dedicated Windows WinForms GUI and native macOS (.NET 8 TUI & CLI host)
-- 64-bit Windows 7 SP1 to Windows 11 compatibility (.NET 4.6.2), plus a macOS
- 14+ portable target for Apple Silicon (Apple M-series, arm64). Intel Macs
- (x86_64) are not supported, and no x64 package is provided.
+- 64-bit Windows 7 SP1 to Windows 11 compatibility (.NET 4.6.2), plus macOS 14+
+ portable targets for Apple Silicon and Intel x86_64. The Intel package has
+ passed Rosetta executable checks; a physical Intel Mac DeX session is not yet
+ verified.
## Design Philosophy
@@ -320,23 +321,24 @@ scripts/Package-Release.ps1
See [DexManager/README.md](DexManager/README.md) for packaging notes.
### 2. macOS Edition (Cross-Platform Host & TUI Dashboard)
-- **Target**: macOS 14 Sonoma or later on Apple Silicon (Apple M-series, arm64).
- Intel Macs (x86_64) are not supported.
-- **Portable package**: `DX-Manager-v2.0.0-macos-arm64.zip`
+- **Target**: macOS 14 Sonoma or later (Apple Silicon & Intel x86_64)
+- **Portable packages**: `DX-Manager-v2.0.0-macos-arm64.zip` and
+ `DX-Manager-v2.0.0-macos-x64.zip`
- **End-user prerequisites**: no Homebrew and no separate .NET installation;
- the package includes a self-contained .NET runtime, scrcpy 4.1, ADB, and the
+ the packages include a self-contained .NET runtime, scrcpy 4.1, ADB, and the
scrcpy server
- **Developer toolchain**: pinned .NET 8 SDK (`global.json`)
- **Solution**: `DexManager.Mac.sln`
-- **Developer packaging output**: `dist/DX-Manager-v2.0.0-macos-arm64.zip`
+- **Developer packaging output**: `dist/DX-Manager-v2.0.0-macos-*.zip`
- **Features**: Interactive terminal UI (TUI) dashboard, CLI argument mode (`--dex`, `--diag`), native macOS path resolution, and multi-device support.
-Users download the arm64 package, extract the complete ZIP, and double-click
-`Start DX Manager.command`. They do not build the source. A version tag such as
-`v2.0.0` is configured to trigger a fresh Apple Silicon build. When the job
-succeeds, the workflow creates a draft GitHub Release containing the verified
-ZIP and its SHA-256 file. A maintainer reviews the draft before publishing it.
-The first remote workflow run for this change still needs to be confirmed.
+Users download the package matching their Mac, extract the complete ZIP, and
+double-click `Start DX Manager.command`. They do not build the source. A
+version tag such as `v2.0.0` is configured to trigger fresh Apple Silicon and
+Intel builds. When both jobs succeed, the workflow creates a draft GitHub
+Release containing both verified ZIPs and their SHA-256 files. A maintainer
+reviews the draft before publishing it. The first remote workflow run for this
+change still needs to be confirmed.
The following commands are for maintainers who need to reproduce the packages
locally:
@@ -344,11 +346,14 @@ locally:
```bash
# Build and verify a prebuilt Apple Silicon portable ZIP
scripts/Package-Mac-Release.sh --rid osx-arm64
+
+# Build and verify a prebuilt Intel portable ZIP
+scripts/Package-Mac-Release.sh --rid osx-x64
```
The repository workflow is configured to run the build and test suites on
-an Apple Silicon runner. Pull requests retain the package as a workflow
-artifact; version tags create a draft Release with the same verified files.
-See the [portable package guide](docs/PACKAGE_README_MACOS.md)
+matching Apple Silicon and Intel runners. Pull requests retain both packages as
+workflow artifacts; version tags create a draft Release with the same verified
+files. See the [portable package guide](docs/PACKAGE_README_MACOS.md)
and the detailed [macOS guide](docs/MACOS_GUIDE.md).
## Project Status
@@ -470,8 +475,8 @@ DX Manager는 개인적으로 사용하던 Batch 스크립트, CMD 명령과 Aut
빠른 설정 타일과 2 × 1 홈 위젯을 제공하는 공식 검증형 DX Companion 번들
- Windows 및 macOS 2가지 플랫폼 전용 빌드 지원 (Windows WinForms GUI & macOS .NET 8 TUI 대시보드/CLI)
- 64비트 Windows 7 SP1~11 (.NET 4.6.2) 호환 및 macOS 14 Sonoma 이상
- Apple Silicon(Apple M 시리즈, arm64)용 포터블 패키지 제공. Intel
- Mac(x86_64)은 지원하지 않으며 x64 패키지를 제공하지 않습니다.
+ Apple Silicon/Intel용 포터블 패키지 제공 목표. Intel 패키지는 Rosetta 기동을
+ 확인했으며 실제 Intel Mac의 DeX 실기는 아직 확인하지 않았습니다.
## 개발 철학
@@ -691,32 +696,35 @@ scripts/Package-Release.ps1
배포 파일 구성은 [DexManager/README.md](DexManager/README.md)를 참조하십시오.
### 2. macOS 에디션 (크로스플랫폼 호스트 & TUI 대시보드)
-- **지원 환경**: macOS 14 Sonoma 이상 Apple Silicon(Apple M 시리즈, arm64).
- Intel Mac(x86_64)은 지원하지 않습니다.
-- **포터블 패키지**: `DX-Manager-v2.0.0-macos-arm64.zip`
+- **지원 환경**: macOS 14 Sonoma 이상 (Apple Silicon 및 Intel x86_64)
+- **포터블 패키지**: `DX-Manager-v2.0.0-macos-arm64.zip` 및
+ `DX-Manager-v2.0.0-macos-x64.zip`
- **사용자 사전 설치**: Homebrew와 별도 .NET 설치 불필요. self-contained
.NET 런타임, scrcpy 4.1, ADB와 scrcpy 서버를 ZIP에 포함
- **개발 환경**: `global.json`에 고정된 .NET 8 SDK
- **솔루션**: `DexManager.Mac.sln`
-- **개발자용 패키징 산출물**: `dist/DX-Manager-v2.0.0-macos-arm64.zip`
+- **개발자용 패키징 산출물**: `dist/DX-Manager-v2.0.0-macos-*.zip`
- **특징**: ANSI 대화형 콘솔 대시보드(TUI), CLI 인자 실행 모드(`--dex`, `--diag`), macOS 표준 경로 및 다중 기기 독립 세션 제어.
-일반 사용자는 arm64 패키지를 내려받아 ZIP 전체의 압축을 풀고
+일반 사용자는 Mac에 맞는 패키지를 내려받아 ZIP 전체의 압축을 풀고
`Start DX Manager.command`를 더블클릭합니다. 소스 빌드는 필요하지 않습니다.
-`v2.0.0` 같은 버전 태그를 push하면 Apple Silicon용 패키지를 새로 빌드하도록
-구성했습니다. 작업이 성공하면 검증된 ZIP과 SHA-256 파일이 포함된 GitHub Release
-초안을 만들며, 유지관리자가 확인한 뒤 공개합니다. 이 변경의 첫 원격 workflow 성공
-여부는 아직 확인해야 합니다.
+`v2.0.0` 같은 버전 태그를 push하면 Apple Silicon용과 Intel용 패키지를 새로
+빌드하도록 구성했습니다. 두 작업이 성공하면 검증된 ZIP 두 개와 SHA-256 파일이
+포함된 GitHub Release 초안을 만들며, 유지관리자가 확인한 뒤 공개합니다. 이 변경의
+첫 원격 workflow 성공 여부는 아직 확인해야 합니다.
다음 명령은 패키지를 로컬에서 재현하려는 유지관리자용입니다.
```bash
# Apple Silicon 포터블 ZIP 미리 빌드 및 검증
scripts/Package-Mac-Release.sh --rid osx-arm64
+
+# Intel 포터블 ZIP 미리 빌드 및 검증
+scripts/Package-Mac-Release.sh --rid osx-x64
```
-저장소의 GitHub Actions workflow는 Apple Silicon 실행 환경에서 빌드·테스트하도록
-구성되어 있습니다. PR에서는 arm64 패키지를 workflow artifact로 보관하고, 버전
-태그에서는 같은 검증 파일을 넣은 Release 초안을 만듭니다.
+저장소의 GitHub Actions workflow는 Apple Silicon과 Intel 실행 환경에서 각각
+빌드·테스트하도록 구성되어 있습니다. PR에서는 두 패키지를 workflow artifact로
+보관하고, 버전 태그에서는 같은 검증 파일을 넣은 Release 초안을 만듭니다.
[포터블 패키지 안내](docs/PACKAGE_README_MACOS.md)와 [macOS
가이드](docs/MACOS_GUIDE.md)를 참조하십시오.
diff --git a/docs/AI_WORKFLOW.md b/docs/AI_WORKFLOW.md
index 7246a83..7715455 100644
--- a/docs/AI_WORKFLOW.md
+++ b/docs/AI_WORKFLOW.md
@@ -48,22 +48,23 @@
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\Package-Release.ps1
```
-macOS 공개 후보는 사용자가 빌드하지 않아도 되도록 Apple Silicon arm64
-self-contained ZIP을 미리 만든다. Intel Mac용 x64 ZIP은 만들지 않는다.
+macOS 공개 후보는 사용자가 빌드하지 않아도 되도록 아키텍처별 self-contained
+ZIP을 미리 만든다.
```bash
scripts/Package-Mac-Release.sh --rid osx-arm64
+scripts/Package-Mac-Release.sh --rid osx-x64
```
-macOS 스크립트는 현재 저장소의 다른 아키텍처용 개발 도구를 그대로 복사하지
-않는다. DX Manager와 `DXMAdbProxy`를 `osx-arm64` RID로 publish하고, SHA-256을
-고정한 공식 scrcpy 4.1 macOS arm64 정적 아카이브를 넣는다. ZIP을 다시 풀어 실행
+macOS 스크립트는 현재 저장소의 x86 전용 개발 도구를 그대로 복사하지 않는다.
+DX Manager와 `DXMAdbProxy`를 대상 RID로 publish하고, SHA-256을 고정한 공식
+scrcpy 4.1 macOS 정적 아카이브를 아키텍처별로 넣는다. ZIP을 다시 풀어 실행
권한, Mach-O 아키텍처, 외부 Homebrew·사용자 경로 의존성, 버전, 필수 문서와
사용자 데이터·디버그 파일 제외를 확인한다. 파일 권한·순서·시각은
`SOURCE_DATE_EPOCH` 또는 현재 Git 커밋 시각으로 정규화한다. GitHub Actions
-workflow는 Apple Silicon `macos-15`에서 arm64 패키지를 빌드·테스트해 PR
-artifact로 보관한다. `v*` 버전 태그에서는 ZIP과 체크섬을 다시 검증한 뒤 Release
-초안을 만들고, 유지관리자가 초안을 확인한 뒤 공개한다.
+workflow는 `macos-15`와 `macos-15-intel`에서 두 패키지를 각각 빌드·테스트해
+PR artifact로 보관한다. `v*` 버전 태그에서는 두 ZIP과 체크섬을 다시 검증한 뒤
+Release 초안을 만들고, 유지관리자가 초안을 확인한 뒤 공개한다.
시스템에 .NET Framework 4.6.2 Developer Pack이 없으면 참조 어셈블리 루트를
`-TargetFrameworkRootPath`로 지정하거나 현재 셸의
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 5889dd1..8c60374 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -4,9 +4,8 @@
## 2026-08 - v2.0.0 (macOS Cross-Platform Edition)
-- Apple Silicon arm64용 self-contained 포터블 ZIP 패키징 추가. Intel Mac은
- 지원하지 않으며 x64 ZIP을 제공하지 않음
-- GitHub Actions가 Apple Silicon에서 단위·통합 테스트와 다중 기기 테스트를
+- Apple Silicon arm64와 Intel x64용 self-contained 포터블 ZIP 패키징 추가
+- GitHub Actions가 각 Mac 아키텍처에서 단위·통합 테스트와 다중 기기 테스트를
실행한 뒤 미리 빌드된 ZIP과 SHA-256 파일을 artifact로 업로드하고, 버전
태그에서는 정확한 파일명과 체크섬을 확인해 Release 초안을 만들도록 구성
- 공식 scrcpy 4.1 macOS 정적 아카이브의 SHA-256, Mach-O 아키텍처, 실행 권한,
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
index ac2e6c5..1be9145 100644
--- a/docs/DECISIONS.md
+++ b/docs/DECISIONS.md
@@ -327,14 +327,13 @@ Scrcpy, DeX orchestrator, 단일창, 화면 OFF, Companion 수신기와 PC→휴
보존하는 것이 우선이다. 공통화는 이 경로를 직접 검증하는 테스트가 생긴 뒤 별도
작업으로 수행한다.
-## 2026-08 - macOS는 Apple Silicon arm64 self-contained ZIP으로 배포한다
+## 2026-08 - macOS는 아키텍처별 self-contained ZIP으로 배포한다
macOS 사용자가 저장소를 clone하거나 .NET, Homebrew, scrcpy와 ADB를 직접
설치하도록 하지 않는다. 원본 Windows 프로젝트가 실행 파일과 도구 폴더 전체를
-미리 만든 ZIP으로 배포하는 것과 같은 원칙을 적용한다. 공식 지원과 검증 범위는
-Apple Silicon arm64로 한정하고 `osx-arm64` RID로 DX Manager와 ADB proxy를
-self-contained publish한다. Intel Mac(x86_64)은 지원하지 않으며 universal 또는
-x64 ZIP을 만들지 않는다.
+미리 만든 ZIP으로 배포하는 것과 같은 원칙을 적용한다. Apple Silicon arm64와
+Intel x64는 하나의 universal ZIP으로 합치지 않고 각각 해당 RID로 DX Manager와
+ADB proxy를 self-contained publish한다.
.NET single-file 내부 압축은 사용하지 않는다. macOS arm64 self-contained
실행 파일을 같은 추출 캐시에서 연속 실행할 때 내부 압축을 켠 빌드에서
@@ -342,10 +341,11 @@ x64 ZIP을 만들지 않는다.
연속 실행을 통과했다. 실행 파일은 계속 single-file로 publish하며 최종 ZIP의
표준 압축은 유지한다.
-저장소의 개발용 `tools`에는 다른 아키텍처의 파일이 있을 수 있으므로 공개
-패키징에서 그 폴더를 무조건 복사하지 않는다. 공식 scrcpy 4.1 macOS arm64 정적
-아카이브의 URL과 SHA-256을 고정해 넣고, 번들 도구를 PATH와 Homebrew보다 먼저
-사용한다. GitHub Actions는 Apple Silicon `macos-15`에서 빌드·실행 검증한다.
+저장소의 개발용 `tools`에는 x86 전용 파일이 있을 수 있으므로 공개 패키징에서
+그 폴더를 무조건 복사하지 않는다. 공식 scrcpy 4.1 macOS 정적 아카이브를
+아키텍처별 URL과 SHA-256으로 고정해 넣고, 번들 도구를 PATH와 Homebrew보다 먼저
+사용한다. GitHub Actions도 실제 아키텍처가 일치하는 `macos-15`와
+`macos-15-intel`에서 별도로 빌드·실행 검증한다.
DeX overlay를 만들거나 제거하기 직전에는 선택 화면에 남은 identity만 신뢰하지
않고 해당 ADB transport에서 안정적인 물리 identity를 다시 읽는다. 이미 알고 있던
diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md
index 24b1690..e84448d 100644
--- a/docs/KNOWN_ISSUES.md
+++ b/docs/KNOWN_ISSUES.md
@@ -143,13 +143,14 @@ DX Manager가 만든 화면과 사용자가 개발자 옵션에서 직접 선택
## macOS 포터블 서명과 실기 범위
-macOS arm64 ZIP은 self-contained이므로 Homebrew와 별도 .NET 설치에 의존하지
-않는다. Intel Mac(x86_64)은 지원 범위가 아니며 x64 ZIP을 제공하지 않는다. 다만
-현재 자동화 설정에는 Apple Developer ID 인증서와
+macOS arm64/x64 ZIP은 self-contained이므로 Homebrew와 별도 .NET 설치에
+의존하지 않는다. 다만 현재 자동화 설정에는 Apple Developer ID 인증서와
notarization 자격 증명이 구성되어 있지 않다. 브라우저로 받은 ZIP은 최초 실행
시 Gatekeeper 승인이 필요할 수 있으며, 서명·공증 전에는 모든 macOS
버전에서 경고 없는 최초 실행을 보장하지 않는다. 패키징 스크립트가 quarantine
속성을 자동 삭제하거나 macOS 보안 기능을 우회하지 않는다.
Apple Silicon ZIP은 Apple Silicon Mac에서 실제 바이너리 기동을 확인했다.
-Intel Mac용 빌드·배포·실기 검증은 수행하지 않는다.
+Intel ZIP은 아키텍처와 외부 경로를 검사하고 Apple Silicon Mac의 Rosetta에서
+실행 파일 기동을 확인했지만, Intel 실기 전체 DeX 흐름은 GitHub의
+`macos-15-intel` 자동 빌드와 별도로 실제 Intel Mac 확인이 남아 있다.
diff --git a/docs/MACOS_GUIDE.md b/docs/MACOS_GUIDE.md
index c7cca00..c86266b 100644
--- a/docs/MACOS_GUIDE.md
+++ b/docs/MACOS_GUIDE.md
@@ -17,11 +17,12 @@
## 2. 시스템 요구사항 (Requirements)
- **운영체제**: macOS 14 Sonoma 이상
-- **아키텍처**: Apple Silicon(Apple M 시리즈, arm64) 전용
-- **미지원**: Intel Mac(x86_64)은 지원하지 않으며 x64 ZIP을 제공하지 않습니다.
+- **아키텍처**:
+ - Apple Silicon (Apple M series) - Native ARM64
+ - Intel Mac (x86_64)
- **포터블 ZIP 사용자**: Homebrew, .NET, scrcpy와 ADB를 별도로 설치하지
- 않습니다. Apple Silicon용으로 미리 빌드된 ZIP에 self-contained .NET 런타임,
- scrcpy 4.1, ADB와 scrcpy 서버가 포함됩니다.
+ 않습니다. Mac 아키텍처에 맞게 미리 빌드된 ZIP에 self-contained .NET
+ 런타임, scrcpy 4.1, ADB와 scrcpy 서버가 포함됩니다.
- **소스 개발자**: `global.json`에 지정된 .NET 8 SDK가 필요합니다.
- **지원 스마트폰**:
- Samsung Galaxy 기기 중 Samsung DeX를 지원하는 기기 (Galaxy S시리즈, Note시리즈, Z Fold시리즈, Tab S시리즈 등)
@@ -31,18 +32,20 @@
## 3. 포터블 ZIP 실행 (Portable Release)
-### 3.1 지원 ZIP 확인
+### 3.1 Mac에 맞는 ZIP 선택
- Apple M 시리즈 Mac: `DX-Manager-v-macos-arm64.zip`
+- Intel Mac: `DX-Manager-v-macos-x64.zip`
여기서 ``은 공개된 Release 버전(예: `2.0.0`)으로 바꿉니다. ZIP 안의
문서에는 패키징 시 실제 버전이 자동 반영됩니다.
-저장소의 GitHub Actions workflow는 `macos-15` Apple Silicon 실행 환경에서 전체
-빌드·테스트·패키지 검증을 수행하도록 구성되어 있습니다. 버전 태그 작업이 성공하면
-검증된 arm64 ZIP과 SHA-256 파일을 포함한 GitHub Release 초안을 만들며,
-유지관리자가 확인 후 공개합니다. 이 변경의 첫 원격 workflow 성공 여부는 아직
-확인해야 합니다. Release가 공개된 뒤 사용자는 arm64 ZIP을
+저장소의 GitHub Actions workflow는 `macos-15` Apple Silicon 실행 환경과
+`macos-15-intel` Intel 실행 환경에서 각각 전체 빌드·테스트·패키지 검증을
+수행하도록 구성되어 있습니다. 버전 태그의 두 작업이 성공하면 검증된 ZIP 두 개와
+SHA-256 파일을 포함한 GitHub Release 초안을 만들며, 유지관리자가 확인 후
+공개합니다. 이 변경의 첫 원격 workflow 성공 여부는 아직 확인해야 합니다.
+Release가 공개된 뒤 사용자는 Mac에 맞는 ZIP 하나를
내려받아 전체 폴더의 압축을 풀고 `Start DX Manager.command`를 더블클릭합니다.
소스 빌드는 필요하지 않습니다.
@@ -89,14 +92,17 @@ dotnet test DexManager.Mac.sln -c Release
dotnet run --project DexManager.MultiDeviceTests -c Release
```
-### 4.3 Apple Silicon 포터블 ZIP 생성
+### 4.3 아키텍처별 포터블 ZIP 생성
```bash
# Apple Silicon용 self-contained ZIP
scripts/Package-Mac-Release.sh --rid osx-arm64
+
+# Intel용 self-contained ZIP
+scripts/Package-Mac-Release.sh --rid osx-x64
```
-스크립트는 DX Manager와 ADB proxy를 `osx-arm64` RID로 미리 publish하고, 공식
+스크립트는 DX Manager와 ADB proxy를 지정한 RID로 미리 publish하고, 공식
scrcpy 4.1 정적 빌드의 SHA-256을 확인한 뒤 번들합니다. 생성한 ZIP을 새 임시
폴더에 다시 풀어 실행 권한, CPU 아키텍처, 외부 Homebrew 경로 의존성, 버전,
라이선스와 사용자 데이터 제외 여부를 검사합니다.
diff --git a/docs/PACKAGE_README_MACOS.md b/docs/PACKAGE_README_MACOS.md
index a0f87f4..d21d7f7 100644
--- a/docs/PACKAGE_README_MACOS.md
+++ b/docs/PACKAGE_README_MACOS.md
@@ -5,19 +5,21 @@ complete archive and run it from the extracted folder. The package includes the
DX Manager runtime, scrcpy 4.1, ADB, and the scrcpy server. Homebrew and a
separate .NET installation are not required.
-The portable package requires **macOS 14 Sonoma or later** and an Apple
-Silicon Mac.
+The portable packages require **macOS 14 Sonoma or later**.
## English
-### 1. Confirm that the Mac is supported
+### 1. Choose the correct package
-- **Supported:** Apple Silicon Macs with an Apple M-series chip. Use the package
- labeled **arm64**.
-- **Not supported:** Intel Macs (x86_64). An x64 package is not provided.
+- **Apple Silicon Mac:** choose the package labeled **arm64**. This applies to
+ Macs with an Apple M-series chip.
+- **Intel Mac:** choose the package labeled **x64** or **x86_64**.
If you are not sure which Mac you have, open the Apple menu and select **About
-This Mac**. Continue only when the window shows an Apple chip.
+This Mac**. The window shows either an Apple chip or an Intel processor.
+
+The two packages provide the same DX Manager functions. Only their processor
+architecture is different.
### 2. Verify the downloaded ZIP
@@ -32,8 +34,9 @@ shasum -a 256 -c "DX-Manager-v${release_version}-macos-arm64.zip.sha256"
When reading this source document on GitHub, replace `` with the
Release version shown in the downloaded filename, for example `2.0.0`. The
README inside a packaged ZIP already contains the exact package version.
-Continue only when the command prints `OK`. If it reports a mismatch, do not
-run the ZIP; download both files again from the Release.
+For an Intel Mac, replace `arm64` with `x64`. Continue only when the command
+prints `OK`. If it reports a mismatch, do not run the ZIP; download both files
+again from the Release.
### 3. Extract and keep the complete folder
@@ -95,14 +98,15 @@ on every supported Mac and macOS version cannot be guaranteed.
### Package contents and limits
-- Bundled DX Manager runtime for Apple Silicon arm64
+- Bundled DX Manager runtime for the selected Mac architecture
- Bundled scrcpy 4.1, ADB, and scrcpy server
- No Homebrew requirement
- No separate .NET installation requirement
- No DX Companion APK in the current macOS package
-Intel Macs are outside the supported scope, and no Intel x64 executable is
-included in the release.
+The Intel executable has been checked under Rosetta on Apple Silicon, while a
+complete Galaxy DeX session on physical Intel Mac hardware still needs direct
+verification.
Do not add, replace, or delete bundled ADB and scrcpy files unless the package
instructions for a later version explicitly require it.
@@ -116,17 +120,18 @@ DX Manager macOS 버전은 포터블 ZIP 패키지로 배포됩니다. ZIP 전
ADB와 scrcpy 서버가 포함됩니다. Homebrew와 별도의 .NET 설치는 필요하지
않습니다.
-포터블 패키지는 **macOS 14 Sonoma 이상 Apple Silicon Mac**에서 사용할 수
-있습니다.
+포터블 패키지의 최소 운영체제는 **macOS 14 Sonoma**입니다.
-### 1. 지원 대상 Mac 확인
+### 1. Mac에 맞는 패키지 선택
-- **지원:** Apple M 시리즈 칩이 탑재된 Apple Silicon Mac. **arm64**로 표시된
- 패키지를 사용하십시오.
-- **미지원:** Intel Mac(x86_64). x64 패키지는 제공하지 않습니다.
+- **Apple Silicon Mac:** **arm64**로 표시된 패키지를 선택하십시오. Apple M
+ 시리즈 칩이 탑재된 Mac이 여기에 해당합니다.
+- **Intel Mac:** **x64** 또는 **x86_64**로 표시된 패키지를 선택하십시오.
Mac의 종류를 모르면 Apple 메뉴에서 **이 Mac에 관하여**를 여십시오. 이 화면에
-Apple 칩이 표시되는 경우에만 계속하십시오.
+Apple 칩 또는 Intel 프로세서가 표시됩니다.
+
+두 패키지의 DX Manager 기능은 같습니다. 프로세서 아키텍처만 다릅니다.
### 2. 다운로드한 ZIP 확인
@@ -142,8 +147,9 @@ shasum -a 256 -c "DX-Manager-v${release_version}-macos-arm64.zip.sha256"
이 원본 문서를 GitHub에서 읽는 경우 ``을 내려받은 파일명에 표시된
Release 버전(예: `2.0.0`)으로 바꾸십시오. 패키지 ZIP 안의 README에는 실제
패키지 버전이 이미 반영됩니다.
-결과에 `OK`가 표시된 경우에만 계속하십시오. 불일치가 표시되면 실행하지 말고
-Release에서 두 파일을 다시 내려받으십시오.
+Intel Mac에서는 `arm64`를 `x64`로 바꿉니다. 결과에 `OK`가 표시된 경우에만
+계속하십시오. 불일치가 표시되면 실행하지 말고 Release에서 두 파일을 다시
+내려받으십시오.
### 3. ZIP 전체 압축 해제 및 폴더 유지
@@ -204,14 +210,14 @@ macOS 버전에 따라 메뉴 문구와 위치가 다를 수 있습니다. Apple
### 패키지 구성 및 범위
-- Apple Silicon arm64용 DX Manager 런타임
+- 선택한 Mac 아키텍처용 DX Manager 런타임
- 번들 scrcpy 4.1, ADB와 scrcpy 서버
- Homebrew 설치 불필요
- 별도 .NET 설치 불필요
- 현재 macOS 패키지에는 DX Companion APK가 포함되지 않음
-Intel Mac은 지원 범위에 포함하지 않으며, Release에 Intel x64 실행 파일을
-포함하지 않습니다.
+Intel 실행 파일은 Apple Silicon Mac의 Rosetta에서 기동을 확인했습니다. 실제
+Intel Mac에서 Galaxy DeX 전체 흐름을 확인하는 실기 검증은 남아 있습니다.
이후 버전의 패키지 안내에서 명시적으로 요구하지 않는 한 번들 ADB와 scrcpy
파일을 추가, 교체 또는 삭제하지 마십시오.
diff --git a/docs/PROJECT_BRIEF.md b/docs/PROJECT_BRIEF.md
index a1c0251..207a92c 100644
--- a/docs/PROJECT_BRIEF.md
+++ b/docs/PROJECT_BRIEF.md
@@ -28,11 +28,11 @@ GitHub 메인 README와 별도로 HTML이 없는 `docs\PACKAGE_README.md`를 배
폴더의 `README.md`로 사용하며, 모든 이중 언어 문서는 영어 다음 한국어
순서로 작성한다.
-macOS 에디션은 `scripts/Package-Mac-Release.sh`로 Apple Silicon arm64
-self-contained ZIP을 만든다. ZIP에는 arm64로 미리 publish한 DX Manager와 ADB
-proxy, 공식 scrcpy 4.1 정적 빌드, ADB, scrcpy-server, 실행기와 라이선스를
-포함한다. 사용자는 Homebrew나 .NET을 설치하거나 소스를 빌드하지 않고 ZIP 전체를
-풀어 실행한다. Intel Mac(x86_64)은 지원하지 않으며 x64 ZIP을 만들지 않는다.
+macOS 에디션은 `scripts/Package-Mac-Release.sh`로 Apple Silicon arm64와
+Intel x64 self-contained ZIP을 별도로 만든다. 각 ZIP에는 해당 아키텍처로
+미리 publish한 DX Manager와 ADB proxy, 공식 scrcpy 4.1 정적 빌드, ADB,
+scrcpy-server, 실행기와 라이선스를 포함한다. 사용자는 Homebrew나 .NET을
+설치하거나 소스를 빌드하지 않고 ZIP 전체를 풀어 실행한다.
## 완료 기능
diff --git a/docs/README.md b/docs/README.md
index f41debe..c6f1267 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -19,7 +19,7 @@
- `RELEASE_NOTES_v2.0.0.md`: v2.0.0 GitHub Release용 영어·한국어 설명
- `RELEASE_NOTES_v1.3.0.md`: 이전 v1.3.0 GitHub Release 기록
- `PACKAGE_README.md`: 배포 ZIP 루트에 들어가는 HTML 없는 영어·한국어 안내
-- `PACKAGE_README_MACOS.md`: macOS Apple Silicon arm64 포터블 ZIP 전용 영어·한국어 안내
+- `PACKAGE_README_MACOS.md`: macOS arm64/x64 포터블 ZIP 전용 영어·한국어 안내
- `../DXDisplayCleanup`: 번들 선택형 Android 복구·파일 전송 앱 소스와 빌드 문서
사용자용 README는 저장소 루트의 `README.md`, 영어/한국어 설명서는
diff --git a/docs/SESSION.md b/docs/SESSION.md
index b2ff266..4c4bb99 100644
--- a/docs/SESSION.md
+++ b/docs/SESSION.md
@@ -5,20 +5,19 @@
## macOS 포터블 배포 작업
- 작업 브랜치: `codex/macos-portable-release`
-- 원본 저장소의 Draft PR #3에서 검토 중이며 제목과 본문은 한국어로 작성한다.
- Apple Silicon arm64 원격 검증을 마칠 때까지 기존 PR #2에는 댓글이나 상태 변경을
- 하지 않는다.
-- `scripts/Package-Mac-Release.sh`가 `osx-arm64`용 DX Manager와 ADB proxy를
- self-contained single-file로 publish한다.
-- 공식 scrcpy 4.1 arm64 정적 아카이브를 고정 SHA-256으로 검증해 ADB와
- scrcpy-server를 함께 포함한다.
-- `DX-Manager-v2.0.0-macos-arm64.zip`과 해당 `.sha256` 파일을 생성한다.
-- GitHub Actions workflow는 Apple Silicon `macos-15`에서 빌드·95개 테스트·39개
- 다중 기기 테스트·package smoke test를 수행하도록 구성했다. PR에서는 arm64
- artifact를 보관하고, `v*` 태그에서는 정확한 ZIP과 체크섬 두 파일을 검증한 뒤
- Release 초안을 만든다. 첫 원격 run 결과 확인은 남아 있다.
-- Apple Silicon ZIP은 native 실행 검증을 통과했다. Intel Mac(x86_64)은 공식
- 지원 범위에서 제외했고 x64 패키지를 더 이상 빌드·배포하지 않는다.
+- `scripts/Package-Mac-Release.sh`가 `osx-arm64`와 `osx-x64`용 DX Manager와
+ ADB proxy를 self-contained single-file로 publish한다.
+- 공식 scrcpy 4.1 arm64/x86_64 정적 아카이브를 고정 SHA-256으로 검증해
+ ADB와 scrcpy-server를 함께 포함한다.
+- `DX-Manager-v2.0.0-macos-arm64.zip`과
+ `DX-Manager-v2.0.0-macos-x64.zip`, 각 `.sha256` 파일을 생성한다.
+- GitHub Actions workflow는 `macos-15`와 `macos-15-intel`에서 빌드·95개
+ 테스트·39개 다중 기기 테스트·package smoke test를 각각 수행하도록
+ 구성했다. PR에서는 artifact를 보관하고, `v*` 태그에서는 정확한 네 파일과
+ 체크섬을 검증한 뒤 Release 초안을 만든다. 첫 원격 run 결과 확인은 남아 있다.
+- Apple Silicon ZIP은 native 실행 검증을 통과했다. Intel ZIP은 Mach-O x64
+ 검사와 이 Mac의 Rosetta에서 DX Manager, proxy, scrcpy 4.1, ADB 37.0.0
+ 실행을 확인했다. 실제 Intel Mac의 Galaxy DeX 실기는 남아 있다.
- macOS `Q` 종료가 no-op이던 조건을 제거하고 실행 중인 서비스 정지와 DeX
overlay cleanup 완료를 기다리도록 수정했다. CLI `--dex`/`--stop-dex`도 기기
감시가 첫 snapshot을 읽은 뒤 동작하며 시작·자연 종료 대기의 `Ctrl+C`를 실제
diff --git a/docs/TODO.md b/docs/TODO.md
index a268d0a..247cd06 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -2,16 +2,16 @@
## 다음 작업
-- [x] macOS Apple Silicon 포터블 배포 기반
- - [x] arm64 DX Manager와 ADB proxy self-contained single-file publish
- - [x] 공식 scrcpy 4.1 arm64 아카이브 SHA-256 검증 및 번들
+- [x] macOS 아키텍처별 포터블 배포 기반
+ - [x] arm64/x64 DX Manager와 ADB proxy self-contained single-file publish
+ - [x] 공식 scrcpy 4.1 arm64/x86_64 아카이브 SHA-256 검증 및 번들
- [x] ZIP 재압축 해제, 실행 권한, 아키텍처, 외부 경로, 제외 파일 검증
- - [x] Apple Silicon GitHub Actions, PR artifact와 검증된 Release 초안
- - [ ] PR에서 첫 GitHub Actions arm64 원격 실행 결과 확인
+ - [x] Apple Silicon/x64 GitHub Actions matrix, PR artifact와 검증된 Release 초안
+ - [ ] PR에서 첫 GitHub Actions arm64/x64 원격 실행 결과 확인
- [x] `Q` 정상 종료의 DeX·단일창·전송·overlay cleanup 보강
- [x] DeX 시작·정리 전 live identity 재검증과 identity별 보류 정리 분리
- [x] CLI 시작·자연 종료 대기의 `Ctrl+C` 취소 및 종료 경합 보강
- - [x] Intel Mac(x86_64) 지원 범위 제외 및 x64 패키지 미제공 명시
+ - [ ] 실제 Intel Mac에서 Galaxy DeX 시작·중지와 overlay cleanup 실기 확인
- [ ] Apple Developer ID 서명·notarization 자격이 준비되면 공개 ZIP 서명
- [x] v2.0.0 공개 후보 준비
diff --git a/scripts/Package-Mac-Release.sh b/scripts/Package-Mac-Release.sh
index 99ded76..011ff2c 100755
--- a/scripts/Package-Mac-Release.sh
+++ b/scripts/Package-Mac-Release.sh
@@ -23,7 +23,7 @@ usage() {
Create one prebuilt, self-contained DX Manager portable ZIP for macOS.
Usage:
- scripts/Package-Mac-Release.sh --rid osx-arm64 [options]
+ scripts/Package-Mac-Release.sh --rid osx-arm64|osx-x64 [options]
Options:
--version VERSION Package version (default: 2.0.0)
@@ -34,7 +34,7 @@ Options:
Examples:
scripts/Package-Mac-Release.sh --rid osx-arm64
- scripts/Package-Mac-Release.sh --rid osx-arm64 --version 2.0.0
+ scripts/Package-Mac-Release.sh --rid osx-x64 --version 2.0.0
EOF
}
@@ -140,8 +140,14 @@ case "$rid" in
scrcpy_asset_arch="aarch64"
scrcpy_sha256="20fd47c9014dd5e0fa77091f3cb7adbda8445a360c4584aeaa0150b5b3988ff3"
;;
+ osx-x64)
+ package_arch="x64"
+ binary_arch="x86_64"
+ scrcpy_asset_arch="x86_64"
+ scrcpy_sha256="ee2a7223bc8dbdc4f482db1134bcf441178dafb833492b71ca4c22090c58ce72"
+ ;;
*)
- fail "unsupported RID '$rid'; only osx-arm64 is supported"
+ fail "unsupported RID '$rid'; use osx-arm64 or osx-x64"
;;
esac