更新

  • 2020/07/19 更新 EditorInstance.json 说明与方法
  • 2019/06/09 初次发布

介绍

进行自动化打包时,需要将打开当前项目的 Unity 进程关掉,否则 Unity 会报告同一个项目不能开启多个 Unity 进程。

1
2
3
4
5
6
7
It looks like another Unity instance is running with this project open.

Multiple Unity instances cannot open the same project.

If you are sure that there is no running Unity instance with the project open, remove /UnityProject/Temp/UnityLockfile and restart Unity.

Project: /UnityProject

环境

  • Unity 5.6.6f2
  • Windows 7
  • macOS 10.14.4

理论上支持 Unity、Windows、macOS 任意版本。

思路

Unity 记录的 Library/EditorInstance.json

Provide extra functionality for older versions of the Unity Editor (prior to Unity 2017.1): Create a file called Library/EditorInstance.json that contains process information for debugging the Unity Editor. This file is created natively by Unity since 2017.1.

JetBrains/resharper-unity: Unity support for both ReSharper and Rider

Unity 在启动后会在 Library/EditorInstance.json 文件中记录当前打开进程的 ID 信息,可是问题 Windows 与 macOS 下大部分情况下都不会生成此文件,原因不明。

此机制只能在 Unity 2017.1 之后使用。

记录启动进程后返回的进程 ID

在编写打包脚本启动 Unity 进程时,可以记录返回的进程 ID 并保存在文件中,用作下一次打包时关闭进程使用。

可依然存在问题,如果有人手动使用 Unity 打开了当前项目,打包依然会失败。

查找打开 Temp/UnityLockfile 的进程

默认情况下 Unity 打开项目时会创建此文件并保持打开状态。因此通过遍历操作系统内的所有名字为 Unity 的进程,查找其打开的文件中是否有当前项目的 Temp/UnityLockfile 文件,如果有则将进程杀掉。

这种方法可以同时适用于 Windows 与 macOS,稳定实用。

解决方案

使用 psutil 这个跨平台的 Python 库即可轻松解决此问题。这个库的主要功能是封装了不同平台下操作进程的统一接口。

不同操作系统上安装的时候会携带不同的 Native 库,因此需要在所有要支持的平台上安装并提交。

注意:

  1. Unity 进程名字 Windows 与 macOS 不同。
  2. 结束 Unity 进程后需要删除 Temp 目录及其内容,否则打开 Unity 时依然会有错误提示。
  3. 杀掉进程后需要等待一段时间,等待操作系统杀死进程后关闭其打开的文件,否则删除 Temp/UnityLockfile 时会提示文件被占用。
  4. 如果 Unity 是以管理员起动的,那么获得权限时会报错。其实没有必要处理此情况,正常情况下不会有人使用管理员权限打开 Unity,如果有人这么做了,就让打包失败吧。
  5. 终止进程时可以使用 KILL 信号,无需等待进程结束,让操作系统直接回收即可,这样才能安全地执行清理操作。

实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import os
import platform
import sys
import shutil
import json
import time

import psutil

def kill_previous(project_path):
    if not kill_previous_by_json(project_path):
        kill_previous_by_openfile(project_path)


def kill_previous_by_json(project_path):
    editor_instance = os.path.join(project_path, 'Library/EditorInstance.json')
    if not os.path.exists(editor_instance):
        return False

    with open(editor_instance) as f:
        process_id = json.load(f)['process_id']

    print(f'Kill previous open Unity process by json: {process_id}')
    sys.stdout.flush()

    process = psutil.Process(process_id)
    process.kill()

    os.remove(editor_instance)

    return True


def kill_unity(project_path):
    unity_lockfile = os.path.abspath(os.path.join(project_path, 'Temp/UnityLockfile'))
    if not os.path.exists(unity_lockfile):
        return

    if platform.system() == 'Windows':
        unity_name = 'Unity.exe'
    else:
        unity_name = 'Unity'

    for proc in psutil.process_iter():
        try:
            if proc.name() != unity_name:
                continue

            # this returns the list of opened files by the current process
            open_files = proc.open_files()
            if not open_files:
                continue

            found = False
            for open_file in open_files:
                if os.path.abspath(open_file.path) != unity_lockfile:
                    continue

                found = True
                print(f'Kill open Unity process: {proc.pid}')
                sys.stdout.flush()
                proc.kill()
                # Wait for OS to terminate process and close open files
                time.sleep(1)
                break

            if found:
                break

        # This catches a race condition where a process ends
        # before we can examine its files
        except psutil.NoSuchProcess as err:
            print("****", err)

    shutil.rmtree(os.path.join(project_path, 'Temp'))

参考资料