【Bug已解决】Dragging a generated image out of Codex Desktop to Finder/Downloads on macOS causes hard cra
【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,开始拖拽,应用随后清理了 tmp,Finder 来取时文件已不存在。 - 内存对象被释放:图片是内存里的对象,拖拽会话还活着,但对象因作用域结束/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_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("拖拽数据生命周期测试通过。")
九、排查清单("拖出文件崩溃"按顺序查)
- 数据来源:拖拽提供的是临时路径还是内存?拖拽期间它是否仍有效?
- 清理时机:临时文件/对象是在 dragEnd 还是 dragBegin 后被清理?
- 引用持有:内存数据提供时,图片对象引用是否保留到拖拽结束?
- 跨进程:系统/Finder 何时来取数据?你的数据源那时是否还在?
- 崩溃防护:提供数据的系统回调是否有 try/except,异常会不会冒泡成硬崩溃?
- 先存盘验证:先手动保存再拖是否不崩?若是,则确认是数据源生命周期问题。
- 暂存目录:拖拽暂存是否用稳定目录,而非会被自动清理的临时根?
十、小结
"拖图片到 Finder 崩溃"是拖拽进行中数据源被提前释放/删除导致的跨进程悬空访问:系统来取数据时,临时文件已被删或内存对象已回收,错误升级为硬崩溃。修复三层:
- 稳定落盘:拖拽前把图片放到稳定暂存路径,拖拽结束才清理;
- 引用持有:内存数据提供时把对象引用保留到 dragEnd,防止回收;
- 崩溃防护:数据提供回调内吞异常、回退空数据,把硬崩溃降级为拖拽失败。
核心原则:拖放是跨进程、异步、生命周期长的操作,提供的数据源必须在整个拖拽会话期间持续有效,并在会话结束后才释放。任何"拖拽开始就清理资源"的写法,都是在埋硬崩溃的雷。

更多推荐


所有评论(0)