
【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、跨进程数据提供、资源生命周期、临时文件、崩溃防护。一、现象长什么样复现很稳定应用生成了一张图片临时放在内存/某个会被清理的临时路径用户用鼠标把这张图拖出应用窗口放到 Finder 的下载目录拖拽一开始或放到一半应用崩溃有时连带系统 dock/menu 重启假重启观感崩溃栈指向拖放相关的系统调用提供数据给拖拽会话的函数如果先把图片保存到文件再拖则不崩溃——说明问题在拖拽时提供的数据源上。关键线索是先存盘再拖就不崩。说明崩溃和拖拽进行中数据源的状态直接相关未存盘的图片在被系统跨进程读取时原始数据已被释放或临时文件被删接收方读到无效内存/失效路径触发底层崩溃。二、背景把图片拖到 Finder 到底发生了什么macOS 的拖放是跨进程的拖拽会话由系统统一管理接收方Finder在用户放下时才向发送方你的应用请求数据。流程是拖拽开始应用向系统注册我能提供 PNG 数据用户拖动系统可能在任意时刻甚至放下前向应用要数据应用通过**数据提供器data provider / promise**返回图片数据或文件路径Finder 拿到数据后写入目标位置。脆弱点在第 3 步如果应用提供的是一个临时文件路径而该文件在拖拽完成前被应用清理掉或者提供的是内存数据指针但图片对象在拖拽期间被垃圾回收/释放——系统读到的就是悬空数据轻则拖拽失败重则进程崩溃尤其涉及跨进程内存时错误会升级为硬崩溃。三、根因拖拽中数据源被提前释放/删除根因拆解提供临时文件后被删图片存到tmp/xxx.png开始拖拽应用随后清理了 tmpFinder 来取时文件已不存在。内存对象被释放图片是内存里的对象拖拽会话还活着但对象因作用域结束/GC 被回收数据提供器读到悬空引用。数据提供器不持有引用注册拖拽时没把图片对象的引用保留到拖拽结束导致生命周期过短。无崩溃防护提供数据的回调里没有异常保护一旦出错直接冒泡到系统拖放框架引发硬崩溃。删除时机错误应用在拖拽开始而不是拖拽结束时清理资源。下面用最小模型复现提供临时路径后删文件导致取数据失败再给修复。四、最小可运行复现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_okTrue) fd, path tempfile.mkstemp(suffix.png, dirstaging) 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, bIMG) assert src.provide(d1) bIMG 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(拖拽数据生命周期测试通过。)九、排查清单拖出文件崩溃按顺序查数据来源拖拽提供的是临时路径还是内存拖拽期间它是否仍有效清理时机临时文件/对象是在 dragEnd 还是 dragBegin 后被清理引用持有内存数据提供时图片对象引用是否保留到拖拽结束跨进程系统/Finder 何时来取数据你的数据源那时是否还在崩溃防护提供数据的系统回调是否有 try/except异常会不会冒泡成硬崩溃先存盘验证先手动保存再拖是否不崩若是则确认是数据源生命周期问题。暂存目录拖拽暂存是否用稳定目录而非会被自动清理的临时根十、小结拖图片到 Finder 崩溃是拖拽进行中数据源被提前释放/删除导致的跨进程悬空访问系统来取数据时临时文件已被删或内存对象已回收错误升级为硬崩溃。修复三层稳定落盘拖拽前把图片放到稳定暂存路径拖拽结束才清理引用持有内存数据提供时把对象引用保留到 dragEnd防止回收崩溃防护数据提供回调内吞异常、回退空数据把硬崩溃降级为拖拽失败。核心原则拖放是跨进程、异步、生命周期长的操作提供的数据源必须在整个拖拽会话期间持续有效并在会话结束后才释放。任何拖拽开始就清理资源的写法都是在埋硬崩溃的雷。