【Bug已解决】Dragging a generated image out of Codex Desktop to Finder/Downloads on macOS causes hard crash / apparent reboot 解决方案

原始报错:Dragging a generated image out of Codex Desktop to Finder/Downloads on macOS causes hard crash / apparent reboot 场景:在 macOS 上,把应用里生成的一张图片直接用鼠标拖到 Finder / 下载文件夹。拖拽过程中应用硬崩溃("假重启"可能是系统 UI 服务被拖垮后重启)。图片本身是内存里生成的,还没存盘。 关键词:拖放、drag & drop、跨进程数据提供、资源生命周期、临时文件、崩溃防护。

一、现象长什么样

复现很稳定:

  1. 应用生成了一张图片,临时放在内存/某个会被清理的临时路径;
  2. 用户用鼠标把这张图拖出应用窗口,放到 Finder 的下载目录;
  3. 拖拽一开始或放到一半,应用崩溃,有时连带系统 dock/menu 重启("假重启"观感);
  4. 崩溃栈指向拖放相关的系统调用(提供数据给拖拽会话的函数);
  5. 如果先把图片"保存到文件"再拖,则不崩溃——说明问题在"拖拽时提供的数据源"上。

关键线索是"先存盘再拖就不崩"。说明崩溃和拖拽进行中数据源的状态直接相关:未存盘的图片在被系统跨进程读取时,原始数据已被释放或临时文件被删,接收方读到无效内存/失效路径,触发底层崩溃。

二、背景:把图片拖到 Finder 到底发生了什么

macOS 的拖放是跨进程的:拖拽会话由系统统一管理,接收方(Finder)在用户"放下"时才向发送方(你的应用)请求数据。流程是:

  1. 拖拽开始,应用向系统注册"我能提供 PNG 数据";
  2. 用户拖动,系统可能在任意时刻(甚至放下前)向应用要数据;
  3. 应用通过**数据提供器(data provider / promise)**返回图片数据或文件路径;
  4. Finder 拿到数据后写入目标位置。

脆弱点在第 3 步:如果应用提供的是一个临时文件路径,而该文件在拖拽完成前被应用清理掉;或者提供的是内存数据指针,但图片对象在拖拽期间被垃圾回收/释放——系统读到的就是悬空数据,轻则拖拽失败,重则进程崩溃(尤其涉及跨进程内存时,错误会升级为硬崩溃)。

三、根因:拖拽中数据源被提前释放/删除

根因拆解:

  1. 提供临时文件后被删:图片存到 tmp/xxx.png,开始拖拽,应用随后清理了 tmp,Finder 来取时文件已不存在。
  2. 内存对象被释放:图片是内存里的对象,拖拽会话还活着,但对象因作用域结束/GC 被回收,数据提供器读到悬空引用。
  3. 数据提供器不持有引用:注册拖拽时没把图片对象的引用保留到拖拽结束,导致生命周期过短。
  4. 无崩溃防护:提供数据的回调里没有异常保护,一旦出错直接冒泡到系统拖放框架,引发硬崩溃。
  5. 删除时机错误:应用在"拖拽开始"而不是"拖拽结束"时清理资源。

下面用最小模型复现"提供临时路径后删文件导致取数据失败",再给修复。

四、最小可运行复现

import os, tempfile

class DragController:
    def __init__(self):
        self.tmp_path = None

    def begin_drag(self, image_bytes: bytes):
        # 错误:把图片放到临时文件,但马上"清理"(模拟作用域结束)
        fd, path = tempfile.mkstemp(suffix=".png")
        with os.fdopen(fd, "wb") as f:
            f.write(image_bytes)
        self.tmp_path = path
        # 模拟:拖拽开始后应用认为"已交给系统"就把临时文件删了
        os.remove(path)            # 灾难:文件没了

    def provide_data(self) -> bytes:
        # 系统/Finder 来取数据时,文件已不存在
        with open(self.tmp_path, "rb") as f:   # FileNotFoundError
            return f.read()

if __name__ == "__main__":
    dc = DragController()
    dc.begin_drag(b"\x89PNG...")
    try:
        dc.provide_data()
    except FileNotFoundError as e:
        print("取数据失败:", e)     # 临时文件已被删,拖拽崩溃根源

运行抛 FileNotFoundError——真实场景里这个异常发生在系统拖放回调里,会升级为崩溃。

五、方案:拖拽前先落盘到稳定路径,且拖拽结束才清理

第一层:开始拖拽前,把图片复制到一个稳定、拖拽期间不被清理的位置(如专用拖拽暂存目录),并保留引用,直到拖拽会话结束(dragEnd)才删除:

import os, tempfile, shutil

class DragControllerSafe:
    def __init__(self):
        self._held = []              # 持有拖拽期间的文件路径,结束时清理

    def begin_drag(self, image_bytes: bytes) -> str:
        # 落盘到稳定暂存目录(不是会被自动清理的 tmp 根)
        staging = os.path.join(tempfile.gettempdir(), "drag_staging")
        os.makedirs(staging, exist_ok=True)
        fd, path = tempfile.mkstemp(suffix=".png", dir=staging)
        with os.fdopen(fd, "wb") as f:
            f.write(image_bytes)
        self._held.append(path)
        return path                  # 提供这个稳定路径

    def provide_data(self, path: str) -> bytes:
        with open(path, "rb") as f:  # 文件在拖拽期间一直存在
            return f.read()

    def end_drag(self):
        # 拖拽会话彻底结束后才清理
        for p in self._held:
            try:
                os.remove(p)
            except OSError:
                pass
        self._held.clear()

if __name__ == "__main__":
    dc = DragControllerSafe()
    p = dc.begin_drag(b"\x89PNG...")
    print("提供数据:", dc.provide_data(p)[:4])   # b'\x89PNG' 成功
    dc.end_drag()                                # 拖拽结束才清

稳定路径 + 延迟清理,保证 Finder 来取时文件一定在。

六、方案:数据提供器持有图片引用至 dragEnd

第二层:如果走"内存数据提供"而非文件路径,必须把图片对象引用保留到拖拽结束,防止被回收:

class InMemoryDragSource:
    def __init__(self):
        self._active = {}            # drag_id -> 图片字节,持有到结束

    def begin(self, drag_id: str, image_bytes: bytes):
        # 持有引用,绝不提前释放
        self._active[drag_id] = image_bytes

    def provide(self, drag_id: str) -> bytes:
        data = self._active.get(drag_id)
        if data is None:
            raise RuntimeError("拖拽数据已被释放(生命周期 bug)")
        return data

    def end(self, drag_id: str):
        self._active.pop(drag_id, None)   # 结束才释放

if __name__ == "__main__":
    src = InMemoryDragSource()
    src.begin("d1", b"\x89PNG...")
    print("内存提供:", src.provide("d1")[:4])
    src.end("d1")

引用在 end 前始终存在,数据提供器不会读到悬空内存。

七、方案:数据提供回调加崩溃防护

第三层:在提供给系统的拖放回调里加异常保护 + 超时,任何意外都转为"拖拽失败"而不是硬崩溃:

import traceback

class SafeProvider:
    def __init__(self, source: InMemoryDragSource):
        self.source = source

    def provide_safe(self, drag_id: str):
        try:
            return self.source.provide(drag_id)
        except Exception:
            # 绝不把异常抛给系统拖放框架
            traceback.print_exc()
            return b""               # 返回空数据 = 拖拽失败,但不崩

if __name__ == "__main__":
    src = InMemoryDragSource()
    # 即使忘了 begin,也只返回空而非崩溃
    prov = SafeProvider(src)
    print("安全提供(未begin):", prov.provide_safe("missing"))

回调内吞掉异常并回退,把"硬崩溃"降级为"拖拽无数据",保护进程与系统 UI 服务。

八、验证:把"拖拽期间数据可用"锁进测试

def test_file_stable_during_drag():
    dc = DragControllerSafe()
    p = dc.begin_drag(b"\x89PNG...")
    # 拖拽进行中(未 end)数据可取
    assert dc.provide_data(p) == b"\x89PNG..."
    dc.end_drag()                      # 结束才清

def test_inmemory_held_until_end():
    src = InMemoryDragSource()
    src.begin("d1", b"IMG")
    assert src.provide("d1") == b"IMG"
    src.end("d1")
    try:
        src.provide("d1")
        assert False
    except RuntimeError:
        pass                          # 结束后取不到,符合预期

if __name__ == "__main__":
    test_file_stable_during_drag()
    test_inmemory_held_until_end()
    print("拖拽数据生命周期测试通过。")

九、排查清单("拖出文件崩溃"按顺序查)

  1. 数据来源:拖拽提供的是临时路径还是内存?拖拽期间它是否仍有效?
  2. 清理时机:临时文件/对象是在 dragEnd 还是 dragBegin 后被清理?
  3. 引用持有:内存数据提供时,图片对象引用是否保留到拖拽结束?
  4. 跨进程:系统/Finder 何时来取数据?你的数据源那时是否还在?
  5. 崩溃防护:提供数据的系统回调是否有 try/except,异常会不会冒泡成硬崩溃?
  6. 先存盘验证:先手动保存再拖是否不崩?若是,则确认是数据源生命周期问题。
  7. 暂存目录:拖拽暂存是否用稳定目录,而非会被自动清理的临时根?

十、小结

"拖图片到 Finder 崩溃"是拖拽进行中数据源被提前释放/删除导致的跨进程悬空访问:系统来取数据时,临时文件已被删或内存对象已回收,错误升级为硬崩溃。修复三层:

  • 稳定落盘:拖拽前把图片放到稳定暂存路径,拖拽结束才清理;
  • 引用持有:内存数据提供时把对象引用保留到 dragEnd,防止回收;
  • 崩溃防护:数据提供回调内吞异常、回退空数据,把硬崩溃降级为拖拽失败。

核心原则:拖放是跨进程、异步、生命周期长的操作,提供的数据源必须在整个拖拽会话期间持续有效,并在会话结束后才释放。任何"拖拽开始就清理资源"的写法,都是在埋硬崩溃的雷。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐