davinci-resolve-mcp 4.8.21 → 4.8.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
4
4
 
5
+ ## What's New in v4.8.22 — the control panel port check cannot hang on a wedged lsof
6
+
7
+ ### Fixed
8
+
9
+ - **`open_control_panel` could block forever behind an unkillable `lsof`.** The
10
+ port-owner check ran `lsof -iTCP:<port> -sTCP:LISTEN -t` through
11
+ `subprocess.run(timeout=3)`. On macOS, lsof wedges in uninterruptible kernel wait
12
+ (state `U` in `ps`) when a network mount is stale, and a process in that state
13
+ ignores SIGKILL. `subprocess.run`'s timeout path kills the child and then waits
14
+ for it, so the 3-second timeout never returned: the caller hung with the child.
15
+ Measured on 2026-09-26 on the release machine, where 489 lsof processes had been
16
+ stuck for 12 hours and the offline suite sat in this function for 13 minutes.
17
+ `_port_owner_pid` now starts lsof in its own session, polls to the deadline, and
18
+ on expiry sends SIGKILL and abandons the child instead of joining it. stdout is
19
+ read only once `poll()` reports an exit, and the pipe is closed on every path.
20
+ A missing lsof is still `None`, not an exception.
21
+
22
+ ### Tests
23
+
24
+ - `tests/test_port_owner_pid.py`: a fake child whose `poll()` never returns and
25
+ whose `kill()` is a no-op yields `None` within the deadline with `wait()` and
26
+ `communicate()` never called and the pipe closed; an exited child still yields
27
+ its PID; a missing binary yields `None`; and a real subprocess that ignores
28
+ SIGTERM, with `kill` patched out, is left running rather than joined.
29
+
5
30
  ## What's New in v4.8.21 — timeline duration no longer overcounts by one frame
6
31
 
7
32
  ### Fixed
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-4.8.21-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.8.22-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-37%20(389%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-4.8.21-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.8.22-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-37%20(389%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v4.8.21 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.8.22 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "4.8.21"
40
+ VERSION = "4.8.22"
41
41
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
42
42
  # Resolve's scripting bridge loads into newer interpreters on recent builds
43
43
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "4.8.21",
3
+ "version": "4.8.22",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -93,7 +93,7 @@ if not logging.getLogger().handlers:
93
93
  handlers=[logging.StreamHandler()],
94
94
  )
95
95
 
96
- VERSION = "4.8.21"
96
+ VERSION = "4.8.22"
97
97
  logger = logging.getLogger("davinci-resolve-mcp")
98
98
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
99
99
  logger.info(f"Detected platform: {get_platform()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 377-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "4.8.21"
14
+ VERSION = "4.8.22"
15
15
 
16
16
  import base64
17
17
  import os
@@ -17750,24 +17750,57 @@ def _control_panel_remote_version(host: str, port: int, timeout: float = 1.5) ->
17750
17750
  return _control_panel_probe(host, port, timeout).get("version")
17751
17751
 
17752
17752
 
17753
- def _port_owner_pid(host: str, port: int) -> Optional[int]:
17753
+ def _port_owner_pid(host: str, port: int, timeout: float = 3.0) -> Optional[int]:
17754
17754
  """Return PID of the process LISTENing on `port`, or None if free/unknown.
17755
17755
 
17756
17756
  Uses lsof with `-iTCP:<port> -sTCP:LISTEN -t`: one PID per line, no header.
17757
17757
  Host is informational only — lsof matches any local LISTEN socket on that
17758
17758
  port (which is what we care about for port-collision detection).
17759
+
17760
+ The deadline is enforced by polling, not by ``subprocess.run(timeout=)``.
17761
+ On macOS, lsof can wedge in uninterruptible kernel wait (state ``U`` in
17762
+ ``ps``) when a network mount is stale, and a child in that state ignores
17763
+ SIGKILL. ``subprocess.run``'s timeout path kills the child and then WAITS
17764
+ for it, so the caller hung with it — measured 2026-09-26 on the release
17765
+ machine: 489 lsof processes stuck in ``U`` for 12 h, and the offline suite
17766
+ blocked here for 13 min. Now the child is killed on expiry and abandoned
17767
+ rather than joined; stdout is read only once ``poll()`` says it exited.
17759
17768
  """
17760
17769
  import subprocess
17770
+ import time as _t
17761
17771
  try:
17762
- result = subprocess.run(
17772
+ proc = subprocess.Popen(
17763
17773
  ["lsof", "-nP", "-iTCP:" + str(port), "-sTCP:LISTEN", "-t"],
17764
- capture_output=True, timeout=3, text=True, encoding="utf-8",
17765
- errors="replace", check=False,
17766
- stdin=subprocess.DEVNULL,
17774
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
17775
+ stdin=subprocess.DEVNULL, start_new_session=True,
17767
17776
  )
17768
- except (OSError, subprocess.TimeoutExpired):
17777
+ except OSError:
17769
17778
  return None
17770
- for line in (result.stdout or "").splitlines():
17779
+ deadline = _t.monotonic() + max(0.0, float(timeout))
17780
+ out = b""
17781
+ try:
17782
+ while proc.poll() is None:
17783
+ if _t.monotonic() >= deadline:
17784
+ try:
17785
+ proc.kill()
17786
+ except OSError:
17787
+ pass
17788
+ # Deliberately no wait()/communicate(): a wedged lsof never
17789
+ # exits, and joining it is exactly the hang this guards against.
17790
+ return None
17791
+ _t.sleep(0.05)
17792
+ if proc.stdout is not None:
17793
+ try:
17794
+ out = proc.stdout.read() or b""
17795
+ except OSError:
17796
+ out = b""
17797
+ finally:
17798
+ if proc.stdout is not None:
17799
+ try:
17800
+ proc.stdout.close()
17801
+ except OSError:
17802
+ pass
17803
+ for line in out.decode("utf-8", "replace").splitlines():
17771
17804
  line = line.strip()
17772
17805
  if line.isdigit():
17773
17806
  return int(line)