시험판 · 시험 (따로 데이터) 이 사용자 시작 메뉴 바로 가기 · jjmkvmaker:// 를 바꾸거나 지우지 않게 (관리자 지시 10/20, 15 찾음, 버그)

- 원인: 앱은 켤 때마다 StartMenu.ensure() (설정을 끄면 remove()) 로 사용자 시작 메뉴의 JJ_MKVMaker.lnk 와
  HKCU\Software\Classes\jjmkvmaker 를 지금 실행 파일로 바꾼다. JJ_MKVMAKER_DATA (시험판 · 시험의 따로 데이터) 로 켜도 그랬다
  - 시험 폴더의 판을 켜면 사용자 바로 가기 · 알림 누르기가 그 판을 가리키고, 시험 폴더를 지우면 깨졌다 (10.18_001 도 같음)
- "따로 데이터" 는 코드에서 JJ_MKVMAKER_DATA 하나뿐 (시험판 표시 파일 없음) - 그때 StartMenu 가 사용자 영역을 건드리는
  ensure (바로 가기 · 주소 연결) · remove 를 막음. 시험이 따로 준 바로 가기 파일만 쓰는 부름은 그대로.
  켤 때 작업 기록에 한 줄, 환경 설정의 [시작 메뉴에 등록] 아래 글도 그 사실로 (en · ja · zh)
- 사용자 판 (JJ_MKVMAKER_DATA 없음) 동작은 그대로
- 시험: 명령 실행을 가짜로 바꿔 (StartMenu.run) 막힌 동안 아무것도 부르지 않음 · 바로 가기 파일도 시험 폴더로
  (shortcutPathOverride - 시험이 잘못돼도 사용자 것을 지우지 않게). 시험 전후 사용자 바로 가기 · 주소 연결이 그대로인지 확인함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rc2
RyuJongJin 2026-10-21 00:03:12 +09:00
parent 1b3160939c
commit 121dd4350b
7 changed files with 66 additions and 6 deletions

View File

@ -1691,6 +1691,7 @@
"시작할 때 새 버전 확인 (하루 한 번)": "Check for a new version at startup (once a day)",
"시작할 때마다 설정 창 보기": "Show the setup window at every start",
"시즌": "Season",
"시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다": "Started with a test data folder (JJ_MKVMAKER_DATA), so the Start menu shortcut and the jjmkvmaker:// link are left unchanged",
"시험판": "Pre-release",
"실시간 동기화 (lsyncd 처럼)": "Live sync (like lsyncd)",
"실시간 동기화 ({0}): {1} → {2}": "Live sync ({0}): {1} → {2}",

View File

@ -1691,6 +1691,7 @@
"시작할 때 새 버전 확인 (하루 한 번)": "起動時に新しいバージョンを確認(1日1回)",
"시작할 때마다 설정 창 보기": "起動するたびに設定ウィンドウを表示",
"시즌": "シーズン",
"시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다": "テスト用データフォルダー (JJ_MKVMAKER_DATA) で起動したため、スタートメニューのショートカットと jjmkvmaker:// の関連付けは変更しません",
"시험판": "プレリリース",
"실시간 동기화 (lsyncd 처럼)": "リアルタイム同期 (lsyncd のように)",
"실시간 동기화 ({0}): {1} → {2}": "リアルタイム同期 ({0}): {1} → {2}",

View File

@ -1691,6 +1691,7 @@
"시작할 때 새 버전 확인 (하루 한 번)": "启动时检查新版本(每天一次)",
"시작할 때마다 설정 창 보기": "每次启动时显示设置窗口",
"시즌": "季",
"시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다": "以测试数据文件夹 (JJ_MKVMAKER_DATA) 启动,因此不更改开始菜单快捷方式和 jjmkvmaker:// 关联",
"시험판": "预发布",
"실시간 동기화 (lsyncd 처럼)": "实时同步 (类似 lsyncd)",
"실시간 동기화 ({0}): {1} → {2}": "实时同步 ({0}): {1} → {2}",

View File

@ -262,7 +262,10 @@ Future<void> main(List<String> args) async {
// 99: 시작 메뉴 바로 가기 (앱 ID) · jjmkvmaker:// 연결 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다 (설정에서 끔)
StartMenu.setProcessAppId();
final shell0 = services.shell;
if (controller.settings.startMenuShortcut) {
if (StartMenu.userAreaLocked()) {
// 관리자 지시 (10/20): 시험판 · 시험 (따로 데이터) - 사용자 바로 가기 · 주소 연결은 그대로 (StartMenu 가 막음), 작업 기록에 한 줄
controller.note(tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다'));
} else if (controller.settings.startMenuShortcut) {
unawaited(StartMenu.ensure().then((ok) {
if (shell0 is DesktopShell) shell0.toastAppReady = ok;
}));

View File

@ -5,6 +5,8 @@ import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:path/path.dart' as p;
import 'data_dir_override.dart';
/// 99 (사용자 결정): 지금 사용자의 시작 메뉴에 JJ_MKVMaker 바로 가기를 앱 ID (AUMID) 와 함께 만들고,
/// jjmkvmaker:// 주소를 이 앱으로 연결한다 (관리자 권한 없이 - 사용자 영역만).
/// 그래야 Windows 알림의 출처가 "JJ_MKVMaker" 로 보이고, 알림을 누르면 앱이 열린다 (jjmkvmaker://lsync).
@ -12,8 +14,19 @@ class StartMenu {
static const aumid = 'JJ.MKVMaker';
static const protocol = 'jjmkvmaker';
/// 관리자 지시 (10/20, 15 찾음): 시험판 · 시험 (따로 데이터 JJ_MKVMAKER_DATA) 이면 사용자의 시작 메뉴 바로 가기 ·
/// jjmkvmaker:// 를 자기 쪽으로 바꾸거나 지우지 않는다. 예전에는 시험 폴더의 판을 켜면 사용자 바로 가기가 그 판을 가리키고,
/// 시험 폴더를 지우면 시작 메뉴 · 알림 누르기가 깨졌다. 시험이 바꿀 수 있게 함수로 둔다
static bool Function() userAreaLocked = () => dataDirOverride != null;
/// 바로 가기 · 레지스트리를 바꾸는 명령 실행 (시험이 가짜로 바꿔 무엇을 부르는지 본다 - 사용자 영역을 건드리지 않고)
static Future<ProcessResult> Function(String exe, List<String> args) run = Process.run;
/// 시험용: 바로 가기 파일 자리 (null = 사용자의 시작 메뉴). 시험이 잘못돼도 사용자 바로 가기를 지우지 않게
static String? shortcutPathOverride;
/// 바로 가기 파일 (사용자의 시작 메뉴)
static String get shortcutPath => p.join(Platform.environment['APPDATA'] ?? '', 'Microsoft', 'Windows', 'Start Menu',
static String get shortcutPath => shortcutPathOverride ?? p.join(Platform.environment['APPDATA'] ?? '', 'Microsoft', 'Windows', 'Start Menu',
'Programs', 'JJ_MKVMaker.lnk');
/// 이 프로세스의 앱 ID (작업 표시줄 · 알림이 바로 가기와 같은 앱으로 묶이게)
@ -35,10 +48,12 @@ class StartMenu {
/// [lnk] · [registerProtocol]: 시험용 (사용자의 시작 메뉴 · 레지스트리를 건드리지 않고)
static Future<bool> ensure({String? exe, String? lnk, bool registerProtocol = true}) async {
if (!Platform.isWindows) return false;
// 사용자 영역 (시작 메뉴 바로 가기 · HKCU 주소 연결) 을 건드리는 부름만 막는다 - 시험이 따로 준 바로 가기 파일만 쓰면 그대로
if ((lnk == null || registerProtocol) && userAreaLocked()) return false;
final target = exe ?? appLauncher();
final script = _script(lnk ?? shortcutPath, target, registerProtocol: registerProtocol);
try {
final r = await Process.run(
final r = await run(
'powershell.exe',
['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-EncodedCommand', _encode(script)],
).timeout(const Duration(seconds: 60));
@ -61,11 +76,11 @@ class StartMenu {
/// 설정에서 끄면: 바로 가기 · 주소 연결을 지운다 (이 앱이 만든 것만)
static Future<void> remove() async {
if (!Platform.isWindows) return;
if (!Platform.isWindows || userAreaLocked()) return;
try {
final f = File(shortcutPath);
if (await f.exists()) await f.delete();
await Process.run('reg.exe', ['delete', r'HKCU\Software\Classes\' + protocol, '/f']);
await run('reg.exe', ['delete', r'HKCU\Software\Classes\' + protocol, '/f']);
} catch (_) {}
}

View File

@ -1141,7 +1141,9 @@ class _SettingsPageState extends State<SettingsPage> {
}
},
title: Text(tr('시작 메뉴에 등록')),
subtitle: Text(tr('알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)')),
subtitle: Text(StartMenu.userAreaLocked()
? tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다')
: tr('알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)')),
),
SettingTile(
title: Text(tr('종료 (창 닫기 ✕ · 종료 버튼) 를 누르면')),

View File

@ -32,6 +32,43 @@ void main() {
expect(await read('id'), StartMenu.aumid);
}, skip: !Platform.isWindows, timeout: const Timeout(Duration(minutes: 2)));
test('관리자 지시 (10/20): 시험판 · 시험 (따로 데이터) 이면 사용자 시작 메뉴 · jjmkvmaker:// 를 바꾸거나 지우지 않는다', () async {
// 명령은 가짜로만 (사용자 영역을 건드리지 않음) - 무엇을 부르는지만 본다
final calls = <String>[];
final runBefore = StartMenu.run, lockedBefore = StartMenu.userAreaLocked;
final tmp = Directory.systemTemp.createTempSync('jj_lnk_');
addTearDown(() {
StartMenu.run = runBefore;
StartMenu.userAreaLocked = lockedBefore;
StartMenu.shortcutPathOverride = null;
tmp.deleteSync(recursive: true);
});
// 바로 가기 파일도 시험 폴더로 (시험이 잘못돼도 사용자 시작 메뉴의 것을 지우지 않게)
final lnk = p.join(tmp.path, 'JJ_MKVMaker.lnk');
StartMenu.shortcutPathOverride = lnk;
File(lnk).writeAsStringSync('x');
StartMenu.run = (exe, args) async {
calls.add(exe);
return ProcessResult(0, 0, 'JJ_OK', '');
};
StartMenu.userAreaLocked = () => true;
expect(await StartMenu.ensure(), isFalse, reason: '사용자 바로 가기 · 주소 연결');
expect(await StartMenu.ensure(lnk: lnk), isFalse, reason: '주소 연결 (HKCU) 은 사용자 영역');
await StartMenu.remove();
expect(calls, isEmpty, reason: '막힌 동안은 아무 명령도 부르지 않음');
expect(File(lnk).existsSync(), isTrue, reason: '바로 가기를 지우지 않음');
// 시험이 따로 준 바로 가기 파일만 쓰는 것은 그대로
expect(await StartMenu.ensure(exe: Platform.resolvedExecutable, lnk: lnk, registerProtocol: false), isTrue);
expect(calls, ['powershell.exe']);
// 막지 않으면 (사용자 판) 예전처럼 부른다
StartMenu.userAreaLocked = () => false;
calls.clear();
expect(await StartMenu.ensure(), isTrue);
await StartMenu.remove();
expect(calls, ['powershell.exe', 'reg.exe']);
expect(File(lnk).existsSync(), isFalse, reason: '사용자 판에서 끄면 지움 (여기서는 시험 폴더의 것)');
}, skip: !Platform.isWindows);
test('알림을 누르면 오는 주소 (jjmkvmaker://lsync) 는 실시간 동기화 화면 요청', () {
expect(LaunchRequest.parse(['jjmkvmaker://lsync/']).action, LaunchAction.lsync);
expect(LaunchRequest.parse(['jjmkvmaker://lsync/']).files, isEmpty);