From a9fd9af2568b4e896683e640832fb9fbdd728c83 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:52:49 +0300 Subject: [PATCH 01/12] fix: keep runtime data out of temporary EXE folders --- screen_recorder/shared.py | 60 ++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/screen_recorder/shared.py b/screen_recorder/shared.py index fc267b1..fc64d87 100644 --- a/screen_recorder/shared.py +++ b/screen_recorder/shared.py @@ -84,7 +84,7 @@ DXCAM_CAPTURE_ENABLED = False APP_NAME = "ScreenRecorderProWin11" -APP_BUILD = "2026-09-23-nvenc-aq-cli-fix-v34" +APP_BUILD = "2026-09-23-nvenc-startup-hardening-v35" DIAGNOSTIC_SCHEMA = "screen_recorder_diagnostics_v22" PROBLEM_LOGS_FOLDER_NAME = "Логи проблем" NO_AUDIO = "Не записывать" @@ -163,6 +163,40 @@ def get_app_folder(): APP_DIR = get_app_folder() +def is_probably_temporary_path(path): + """True, если путь находится внутри системной временной папки.""" + try: + Path(path).resolve().relative_to(Path(tempfile.gettempdir()).resolve()) + return True + except (OSError, ValueError): + return False + + +def default_recording_output_folder(): + """Стабильная папка записи, даже если EXE запущен из временного каталога.""" + candidates = [] + if not is_probably_temporary_path(APP_DIR): + candidates.append(APP_DIR) + try: + candidates.append(Path.home() / "Videos") + except Exception: + pass + try: + candidates.append(Path.home()) + except Exception: + pass + candidates.append(Path.cwd()) + + for folder in candidates: + try: + folder = Path(folder).expanduser() + folder.mkdir(parents=True, exist_ok=True) + return str(folder) + except Exception: + continue + return str(Path(tempfile.gettempdir()) / APP_NAME) + + def get_bundle_folder(): """Каталог read-only ресурсов текущего source/PyInstaller bundle. @@ -339,7 +373,12 @@ def get_writable_data_root(): %LOCALAPPDATA%/ScreenRecorderProWin11. Это убирает тихие ошибки сохранения настроек/логов/временных файлов. """ - candidates = [APP_DIR] + candidates = [] + # PyInstaller/браузеры/тестовые оболочки иногда запускают EXE из + # %TEMP%\\scoped_dir.... Хранить там settings/logs нельзя: каталог может + # исчезнуть между запусками. Portable-папку сохраняем при обычном запуске. + if not is_probably_temporary_path(APP_DIR): + candidates.append(APP_DIR) try: local = os.getenv("LOCALAPPDATA") or os.getenv("APPDATA") if local: @@ -371,14 +410,15 @@ def get_extended_logs_root(): создаётся отдельная папка с датой и временем записи. """ candidates = [] - try: - candidates.append(APP_DIR / PROBLEM_LOGS_FOLDER_NAME) - except Exception: - pass - try: - candidates.append(APP_DIR.parent / PROBLEM_LOGS_FOLDER_NAME) - except Exception: - pass + if not is_probably_temporary_path(APP_DIR): + try: + candidates.append(APP_DIR / PROBLEM_LOGS_FOLDER_NAME) + except Exception: + pass + try: + candidates.append(APP_DIR.parent / PROBLEM_LOGS_FOLDER_NAME) + except Exception: + pass try: candidates.append(get_writable_data_root() / PROBLEM_LOGS_FOLDER_NAME) except Exception: From e48a637390256af97b4770f9911b790a65fd71ec Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:52:56 +0300 Subject: [PATCH 02/12] fix: persist output defaults outside temporary launch folders --- screen_recorder/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/screen_recorder/app.py b/screen_recorder/app.py index 9b42ec7..e9725d9 100644 --- a/screen_recorder/app.py +++ b/screen_recorder/app.py @@ -125,6 +125,7 @@ def __init__(self, root): }) self.ffmpeg_path = resolve_ffmpeg_path() self._encoder_support_cache = {} + self._encoder_option_support_cache = {} self._filter_support_cache = {} self._input_format_support_cache = {} self._ddagrab_warm_done = False @@ -330,7 +331,7 @@ def __init__(self, root): # плавающая панель специально видна на экране и попадает в итоговое видео. self.annotation_toolbar_clean_frame = None - self.output_folder = tk.StringVar(value=self.settings.get("output_folder", os.getcwd())) + self.output_folder = tk.StringVar(value=self.settings.get("output_folder", default_recording_output_folder())) self.format_var = tk.StringVar(value=self.settings.get("format", "mkv")) self.fps_var = tk.StringVar(value=self.settings.get("fps", "60")) self.auto_adjust_fps_var = tk.BooleanVar(value=bool(self.settings.get("auto_adjust_fps", False))) From 845ee3abd20e9276d8578d30626516ab72cdba68 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:53:22 +0300 Subject: [PATCH 03/12] fix: probe NVENC encoder options during preflight --- screen_recorder/mixins/ffmpeg_support.py | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/screen_recorder/mixins/ffmpeg_support.py b/screen_recorder/mixins/ffmpeg_support.py index 7b1bfa7..4e3158f 100644 --- a/screen_recorder/mixins/ffmpeg_support.py +++ b/screen_recorder/mixins/ffmpeg_support.py @@ -123,6 +123,51 @@ def ffmpeg_supports_encoder(self, encoder_name): self._encoder_support_cache[key] = ok return ok + def ffmpeg_supports_encoder_option(self, encoder_name, option_name): + """Проверяет конкретную CLI-опцию энкодера через ffmpeg -h encoder=NAME. + + NVENC может присутствовать в -encoders, но конкретная сборка FFmpeg + способна отличаться набором/именами AVOptions. Не блокируем GUI: фоновый + preflight заполняет кэш, а при неизвестном результате необязательная + опция просто не добавляется в команду записи. + """ + encoder_key = str(encoder_name or "").strip().lower() + option_key = str(option_name or "").strip().lower().lstrip("-") + cache = getattr(self, "_encoder_option_support_cache", None) + if not isinstance(cache, dict): + cache = {} + self._encoder_option_support_cache = cache + key = (encoder_key, option_key) + if key in cache: + return bool(cache[key]) + if not encoder_key or not option_key: + cache[key] = False + return False + if self.is_gui_thread(): + try: + if self._preflight_thread is None or not self._preflight_thread.is_alive(): + self._preflight_thread = threading.Thread(target=self.preflight_worker, daemon=True) + self._preflight_thread.start() + except Exception: + pass + return False + try: + result = self.run_managed_process( + [self.ffmpeg_path, "-hide_banner", "-h", f"encoder={encoder_key}"], + capture_output=True, + text=True, + encoding="utf-8", + errors="ignore", + timeout=8, + creationflags=self.creation_flags(), + ) + help_text = ((result.stdout or "") + "\n" + (result.stderr or "")).lower() + ok = bool(re.search(r"(?m)^\s*-" + re.escape(option_key) + r"(?=\s|$)", help_text)) + except Exception: + ok = False + cache[key] = ok + return ok + def ffmpeg_supports_filter(self, filter_name): """Проверяем фильтр FFmpeg. Для плавного захвата нужен ddagrab.""" key = str(filter_name).lower() From c936a5fc64882134e487d44190d36eac5dc98522 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:53:32 +0300 Subject: [PATCH 04/12] fix: cache NVENC option support before recording --- screen_recorder/mixins/instant_buffer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/screen_recorder/mixins/instant_buffer.py b/screen_recorder/mixins/instant_buffer.py index 2236045..4c05d50 100644 --- a/screen_recorder/mixins/instant_buffer.py +++ b/screen_recorder/mixins/instant_buffer.py @@ -204,6 +204,11 @@ def preflight_worker(self): return try: self.ffmpeg_supports_encoder("h264_nvenc") + self.ffmpeg_supports_encoder("hevc_nvenc") + for encoder_name in ("h264_nvenc", "hevc_nvenc"): + if self._encoder_support_cache.get(encoder_name): + self.ffmpeg_supports_encoder_option(encoder_name, "spatial-aq") + self.ffmpeg_supports_encoder_option(encoder_name, "temporal-aq") except Exception: pass ddagrab_ok = False @@ -220,6 +225,10 @@ def preflight_worker(self): self.diagnostic_log("preflight_worker_finish", { "ffmpeg_ok": self._ffmpeg_ok_cache, "encoder_support_cache": self._encoder_support_cache, + "encoder_option_support_cache": { + f"{encoder}:{option}": supported + for (encoder, option), supported in self._encoder_option_support_cache.items() + }, "filter_support_cache": self._filter_support_cache, "input_format_support_cache": self._input_format_support_cache, }) From 1794fcf91e182751627660e9674245fe7a7c7ea6 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:53:43 +0300 Subject: [PATCH 05/12] fix: add NVENC AQ flags only when FFmpeg supports them --- screen_recorder/mixins/capture_commands.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/screen_recorder/mixins/capture_commands.py b/screen_recorder/mixins/capture_commands.py index ee061b3..2b608b5 100644 --- a/screen_recorder/mixins/capture_commands.py +++ b/screen_recorder/mixins/capture_commands.py @@ -311,11 +311,14 @@ def append_encoder_options(self, cmd, fps_int, video_bitrate, bufsize, use_nvenc if use_nvenc: # Для захвата экрана важнее равномерно и быстро принимать кадры, чем - # держать большую очередь lookahead. Нулевой lookahead и отключённый - # temporal AQ уменьшают задержку/нагрузку GPU; битрейт уже имеет запас. + # держать большую очередь lookahead. Нулевой lookahead уменьшает + # задержку. AQ-параметры добавляем только если конкретная сборка + # FFmpeg реально показывает их в ffmpeg -h encoder=..., чтобы смена + # FFmpeg не ломала старт записи из-за необязательной AVOption. + encoder_name = "hevc_nvenc" if use_hevc else "h264_nvenc" cmd += [ "-c:v", - "hevc_nvenc" if use_hevc else "h264_nvenc", + encoder_name, "-preset", "fast", "-rc", @@ -328,10 +331,12 @@ def append_encoder_options(self, cmd, fps_int, video_bitrate, bufsize, use_nvenc bufsize2, "-rc-lookahead", "0", - "-spatial-aq", - "1", - "-temporal-aq", - "0", + ] + if self.ffmpeg_supports_encoder_option(encoder_name, "spatial-aq"): + cmd += ["-spatial-aq", "1"] + if self.ffmpeg_supports_encoder_option(encoder_name, "temporal-aq"): + cmd += ["-temporal-aq", "0"] + cmd += [ "-profile:v", "main" if use_hevc else "high", "-bf", From da9972b479b65c8a8a4acc1fbe1560bca69f827f Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:54:02 +0300 Subject: [PATCH 06/12] fix: classify ddagrab startup failures before GDI fallback --- screen_recorder/mixins/processes.py | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/screen_recorder/mixins/processes.py b/screen_recorder/mixins/processes.py index 72b844e..5784c32 100644 --- a/screen_recorder/mixins/processes.py +++ b/screen_recorder/mixins/processes.py @@ -10,6 +10,28 @@ class ProcessMixin: "acquirenextframe failed: 887a0026", "dxgi_error_access_lost", ) + # Сообщения инициализации Desktop Duplication из FFmpeg ddagrab. + # Только эти ошибки оправдывают переключение на gdigrab. Синтаксис CLI, + # неизвестные encoder options и NVENC failures не должны маскироваться GDI. + DDAGRAB_STARTUP_FALLBACK_MARKERS = ( + "failed querying idxgidevice", + "failed getting parent idxgiadapter", + "failed to enumerate dxgi output", + "failed getting output description", + "failed querying idxgioutput1", + "too many open duplication sessions", + "selected output not supported", + "invalid output duplication argument", + "desktop duplication access denied", + "failed duplicating output", + "only 8 bit output supported with legacy api", + "createvertexshader failed", + "createinputlayout failed", + "createpixelshader failed", + "createbuffer const buffer failed", + "createsamplerstate failed", + "createblendstate failed", + ) @staticmethod def classify_recording_video_progress_health( @@ -53,6 +75,12 @@ def classify_ffmpeg_capture_stderr(cls, text, capture_backend): return "dxgi_access_lost" return None + @classmethod + def should_fallback_ddagrab_startup(cls, text): + """True только для ошибок инициализации Desktop Duplication.""" + normalized = str(text or "").lower() + return any(marker in normalized for marker in cls.DDAGRAB_STARTUP_FALLBACK_MARKERS) + @staticmethod def classify_input_desktop_name(name, open_succeeded=True): if not open_succeeded: @@ -207,6 +235,10 @@ def worker(): stderr_log.write(raw_bytes) combined = scan_tail + text + # Нужен для точной классификации мгновенного startup + # failure: fallback захвата не должен скрывать ошибки + # FFmpeg CLI/NVENC. + self.current_ffmpeg_stderr_tail = combined[-4000:] classification = self.classify_ffmpeg_capture_stderr( combined, capture_backend, From b156c3d52d715c418164ff6c1c12a216dacf34eb Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:54:25 +0300 Subject: [PATCH 07/12] fix: avoid masking FFmpeg startup errors with GDI fallback --- screen_recorder/mixins/recording_session.py | 68 ++++++++++++++++++--- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/screen_recorder/mixins/recording_session.py b/screen_recorder/mixins/recording_session.py index 55a16f0..e23b2ab 100644 --- a/screen_recorder/mixins/recording_session.py +++ b/screen_recorder/mixins/recording_session.py @@ -234,6 +234,25 @@ def start_recording(self): except Exception as exc: self.stop_cursor_highlight_overlay() self.log_exception("start_recording", exc) + self.recording_failure_reason = str(exc) + try: + self.write_ai_smoothness_report( + outcome="recording_start_failed", + error_text=str(exc), + ) + except Exception as report_exc: + self.log_exception("start_recording.write_ai_smoothness_report", report_exc) + try: + self.write_ai_problem_summary( + outcome="Ошибка запуска записи", + error_text=str(exc), + ) + except Exception as summary_exc: + self.log_exception("start_recording.write_ai_problem_summary", summary_exc) + try: + self.close_recording_problem_log_session(reason="start_failed") + except Exception: + pass try: self.stop_recording_performance_sampler() except Exception: @@ -368,10 +387,15 @@ def start_new_segment(self): try: self.launch_checked_ffmpeg_segment(segment_path, fallback_backend) except Exception as fb_exc: - # Если ddagrab был выбран из кэша, но всё равно упал, - # последняя безопасная попытка — gdigrab. Без повторных - # ffmpeg -filters проверок в GUI-потоке. - if fallback_backend != "gdigrab": + # Переходим на GDI только если stderr доказывает сбой + # именно Desktop Duplication. Ошибки CLI/NVENC должны + # остаться исходной ошибкой, а не повторяться во второй + # почти такой же FFmpeg-команде. + stderr_tail = getattr(self, "current_ffmpeg_stderr_tail", "") + if ( + fallback_backend == "ddagrab" + and self.should_fallback_ddagrab_startup(stderr_tail) + ): self.log_exception("fallback_capture_backend", fb_exc) try: if segment_path.exists(): @@ -390,8 +414,15 @@ def start_new_segment(self): try: self.launch_checked_ffmpeg_segment(segment_path, capture_backend) except RuntimeError as exc: - if capture_backend == "ddagrab": - self.log_handle.write(f"\n--- FALLBACK ---\nddagrab failed at start: {exc}\nTrying gdigrab...\n") + stderr_tail = getattr(self, "current_ffmpeg_stderr_tail", "") + if ( + capture_backend == "ddagrab" + and self.should_fallback_ddagrab_startup(stderr_tail) + ): + self.log_handle.write( + f"\n--- FALLBACK ---\nddagrab initialization failed: {exc}\n" + "Trying gdigrab because stderr identifies a Desktop Duplication failure.\n" + ) self.log_handle.flush() try: if segment_path.exists(): @@ -400,6 +431,15 @@ def start_new_segment(self): pass self.launch_checked_ffmpeg_segment(segment_path, "gdigrab") else: + self.diagnostic_log( + "ffmpeg_start_failure_no_capture_fallback", + { + "capture_backend": capture_backend, + "error": str(exc), + "stderr_tail": str(stderr_tail)[-2000:], + }, + level="ERROR", + ) raise except Exception: if python_loopback_started: @@ -467,6 +507,7 @@ def launch_checked_ffmpeg_segment(self, segment_path, capture_backend): self.current_segment_last_video_frame_advance_perf = None self.current_segment_last_video_frame_out_time_seconds = None self.current_segment_video_stall_detected = False + self.current_ffmpeg_stderr_tail = "" self.start_ffmpeg_stderr_reader( process, log_path=self.get_current_recording_log_path(), @@ -502,7 +543,20 @@ def launch_checked_ffmpeg_segment(self, segment_path, capture_backend): pass self.wait_for_ffmpeg_stderr_reader(process, timeout=1.0) self.unregister_child_process(process) - raise RuntimeError(f"FFmpeg сразу завершился с кодом {code}. Лог: {self.current_log_path}") + stderr_excerpt = str(getattr(self, "current_ffmpeg_stderr_tail", "") or "").strip() + self.diagnostic_log( + "ffmpeg_recording_segment_start_failed", + { + "capture_backend": capture_backend, + "returncode": code, + "stderr_tail": stderr_excerpt[-2000:], + }, + level="ERROR", + ) + message = f"FFmpeg сразу завершился с кодом {code}. Лог: {self.current_log_path}" + if stderr_excerpt: + message += "\nFFmpeg stderr:\n" + stderr_excerpt[-1500:] + raise RuntimeError(message) self.current_segment_engine = "ffmpeg" with self.process_lock: From 327d144d7a913bd81680f17e008c715fedcdeb36 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:54:42 +0300 Subject: [PATCH 08/12] fix: migrate scoped temp output folders to a persistent default --- screen_recorder/app.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/screen_recorder/app.py b/screen_recorder/app.py index e9725d9..4ae8461 100644 --- a/screen_recorder/app.py +++ b/screen_recorder/app.py @@ -331,7 +331,21 @@ def __init__(self, root): # плавающая панель специально видна на экране и попадает в итоговое видео. self.annotation_toolbar_clean_frame = None - self.output_folder = tk.StringVar(value=self.settings.get("output_folder", default_recording_output_folder())) + saved_output_folder = self.settings.get("output_folder", default_recording_output_folder()) + try: + saved_output_text = str(saved_output_folder or "").strip() + if ( + is_probably_temporary_path(saved_output_text) + and ( + "scoped_dir" in saved_output_text.lower() + or Path(saved_output_text).resolve() == APP_DIR.resolve() + or Path(saved_output_text).resolve() == Path.cwd().resolve() + ) + ): + saved_output_folder = default_recording_output_folder() + except Exception: + pass + self.output_folder = tk.StringVar(value=saved_output_folder) self.format_var = tk.StringVar(value=self.settings.get("format", "mkv")) self.fps_var = tk.StringVar(value=self.settings.get("fps", "60")) self.auto_adjust_fps_var = tk.BooleanVar(value=bool(self.settings.get("auto_adjust_fps", False))) From 4b32ef04d45d3146d61cc1ddce15dc3481f74b09 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:54:59 +0300 Subject: [PATCH 09/12] test: cover NVENC option probing and fallback classification --- verify_project.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/verify_project.py b/verify_project.py index f47e01f..4dc7966 100644 --- a/verify_project.py +++ b/verify_project.py @@ -73,6 +73,8 @@ def main() -> int: NO_AUDIO, Image, RECORDING_CURSOR_SIZE_PERCENT_OPTIONS, + default_recording_output_folder, + is_probably_temporary_path, normalize_recording_cursor_size_percent, normalize_screenshot_annotation_color, normalize_screenshot_annotation_size, @@ -96,6 +98,8 @@ def main() -> int: "summarize_python_loopback_audio_sync", "classify_visual_motion_window", "classify_recording_video_progress_health", "classify_ffmpeg_capture_stderr", + "should_fallback_ddagrab_startup", + "ffmpeg_supports_encoder_option", "classify_input_desktop_name", "should_restart_after_capture_access_lost", "capture_signal_matches", @@ -169,6 +173,10 @@ def main() -> int: return 1 encoder_command = [] cursor_app.should_use_hevc = lambda: False + cursor_app._encoder_option_support_cache = { + ("h264_nvenc", "spatial-aq"): True, + ("h264_nvenc", "temporal-aq"): True, + } cursor_app.append_encoder_options( encoder_command, 72, @@ -194,6 +202,41 @@ def main() -> int: ): print("ОШИБКА: NVENC AQ options должны использовать синтаксис FFmpeg с дефисами:", encoder_command) return 1 + cursor_app._encoder_option_support_cache = { + ("h264_nvenc", "spatial-aq"): False, + ("h264_nvenc", "temporal-aq"): False, + } + no_aq_command = [] + cursor_app.append_encoder_options( + no_aq_command, + 72, + "16M", + "32M", + use_nvenc=True, + capture_backend="ddagrab", + ) + if "-spatial-aq" in no_aq_command or "-temporal-aq" in no_aq_command: + print("ОШИБКА: неподдерживаемые NVENC AQ options попали в команду:", no_aq_command) + return 1 + + if not cursor_app.should_fallback_ddagrab_startup("Selected output not supported"): + print("ОШИБКА: доказанный ddagrab startup failure не разрешает GDI fallback.") + return 1 + if cursor_app.should_fallback_ddagrab_startup("Unrecognized option 'spatial_aq'. Option not found"): + print("ОШИБКА: синтаксическая ошибка FFmpeg ошибочно разрешает GDI fallback.") + return 1 + if cursor_app.should_fallback_ddagrab_startup("Error while opening encoder h264_nvenc"): + print("ОШИБКА: NVENC failure ошибочно разрешает GDI fallback.") + return 1 + + scoped_probe = Path(tempfile.gettempdir()) / "scoped_dir_regression_probe" + if not is_probably_temporary_path(scoped_probe): + print("ОШИБКА: scoped temp path не распознаётся как временный.") + return 1 + if not str(default_recording_output_folder()).strip(): + print("ОШИБКА: не удалось выбрать устойчивую папку записи по умолчанию.") + return 1 + cursor_app.recording_refresh_hz = 144 if cursor_app.get_ddagrab_poll_fps(72) != 144: print("ОШИБКА: 144 Гц / 72 FPS больше не даёт ddagrab poll 144.") From 8597e0b964294c97b11e7eb7508c94575d96575d Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:56:12 +0300 Subject: [PATCH 10/12] fix: use embedded sources for packaged error snapshots --- screen_recorder/shared.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/screen_recorder/shared.py b/screen_recorder/shared.py index fc64d87..d1fbe9a 100644 --- a/screen_recorder/shared.py +++ b/screen_recorder/shared.py @@ -214,6 +214,18 @@ def get_bundle_folder(): BUNDLE_DIR = get_bundle_folder() +def get_source_snapshot_root(): + """Корень читаемых исходников для диагностического snapshot/manifest.""" + if getattr(sys, "frozen", False): + try: + embedded = BUNDLE_DIR / "embedded_source" + if embedded.is_dir(): + return embedded + except Exception: + pass + return APP_DIR + + def resolve_packaged_tool(tool_name): """Находит bundled executable рядом с кодом/EXE, затем пробует PATH.""" base_name = str(tool_name or "").strip() @@ -277,7 +289,7 @@ def get_program_entry_path(): def write_modular_source_snapshot(target_path, max_bytes=5_000_000): """Сохраняет все исходники проекта в один читаемый нейросетью .py-файл.""" target_path = Path(target_path) - root = APP_DIR + root = get_source_snapshot_root() chunks = [ "# SCREEN RECORDER PRO — MODULAR SOURCE SNAPSHOT\n", f"# Project root: {root}\n\n", @@ -325,7 +337,7 @@ def write_modular_source_snapshot(target_path, max_bytes=5_000_000): def write_modular_source_manifest(target_path): """Пишет компактный список исходников и SHA-256 вместо копии всего кода.""" target_path = Path(target_path) - root = APP_DIR + root = get_source_snapshot_root() files = [] for path in root.rglob("*.py"): try: @@ -353,6 +365,11 @@ def write_modular_source_manifest(target_path): "schema": "screen_recorder_source_manifest_v1", "app_build": APP_BUILD, "project_root": str(root), + "source_root_kind": ( + "embedded_packaged_source" + if getattr(sys, "frozen", False) and root != APP_DIR + else "live_project_source" + ), "created_at": datetime.now().isoformat(timespec="seconds"), "file_count": len(files), "files": files, From 9363f3d23dca6978b956fc6f11961056256f7285 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:56:19 +0300 Subject: [PATCH 11/12] build: embed readable sources for error diagnostics --- scripts/build_windows_exe.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/build_windows_exe.ps1 b/scripts/build_windows_exe.ps1 index 8dabe06..fbabf63 100644 --- a/scripts/build_windows_exe.ps1 +++ b/scripts/build_windows_exe.ps1 @@ -78,6 +78,9 @@ $pyInstallerArgs = @( "--name", $OutputName, "--add-binary", "$ffmpegPath;.", "--add-binary", "$ffprobePath;.", + "--add-data", "$projectRoot\screen_recorder;embedded_source\screen_recorder", + "--add-data", "$projectRoot\main.py;embedded_source", + "--add-data", "$projectRoot\Screen Recorder Pro.py;embedded_source", "Screen Recorder Pro.py" ) From 1e66eb0adb8e92a9662721c2e4008fc66cbb7979 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnichenko <39602114+zeter1@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:56:26 +0300 Subject: [PATCH 12/12] test: verify packaged diagnostic sources are present --- main.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 7834932..cc68221 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ from screen_recorder.shared import ( APP_BUILD, SingleInstanceGuard, + get_source_snapshot_root, resolve_ffmpeg_path, resolve_ffprobe_path, ) @@ -57,7 +58,21 @@ def report(message, *, error=False): ) return 23 - report(f"PACKAGING_SMOKE_OK build={APP_BUILD}") + source_root = get_source_snapshot_root() + try: + embedded_sources = list(Path(source_root).rglob("*.py")) + except Exception: + embedded_sources = [] + if not embedded_sources: + report( + f"PACKAGING_SMOKE_FAIL: diagnostic source snapshot is empty: {source_root}", + error=True, + ) + return 24 + + report( + f"PACKAGING_SMOKE_OK build={APP_BUILD} embedded_sources={len(embedded_sources)}" + ) return 0