mcpywrap-core 0.6.0__py3-none-any.whl

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.
Files changed (189) hide show
  1. mcpywrap_core/__init__.py +2 -0
  2. mcpywrap_core/__main__.py +18 -0
  3. mcpywrap_core/_script_worker.py +278 -0
  4. mcpywrap_core/_version.py +1 -0
  5. mcpywrap_core/api.py +69 -0
  6. mcpywrap_core/builders/AddonsPack.py +310 -0
  7. mcpywrap_core/builders/MapPack.py +171 -0
  8. mcpywrap_core/builders/__init__.py +20 -0
  9. mcpywrap_core/builders/assembly.py +100 -0
  10. mcpywrap_core/builders/dependency_manager.py +239 -0
  11. mcpywrap_core/builders/file_merge.py +260 -0
  12. mcpywrap_core/builders/project_builder.py +182 -0
  13. mcpywrap_core/builders/watcher.py +162 -0
  14. mcpywrap_core/cli.py +75 -0
  15. mcpywrap_core/code_libraries.py +201 -0
  16. mcpywrap_core/command_context.py +242 -0
  17. mcpywrap_core/commands/__init__.py +1 -0
  18. mcpywrap_core/commands/add_cmd.py +76 -0
  19. mcpywrap_core/commands/build_cmd.py +15 -0
  20. mcpywrap_core/commands/connect_cmd.py +46 -0
  21. mcpywrap_core/commands/default_cmd.py +14 -0
  22. mcpywrap_core/commands/dependency_prompt.py +45 -0
  23. mcpywrap_core/commands/deployment_cmd.py +115 -0
  24. mcpywrap_core/commands/deployment_output.py +289 -0
  25. mcpywrap_core/commands/desktop_cmd.py +104 -0
  26. mcpywrap_core/commands/dev_cmd.py +108 -0
  27. mcpywrap_core/commands/doctor_cmd.py +32 -0
  28. mcpywrap_core/commands/edit_cmd.py +109 -0
  29. mcpywrap_core/commands/engine_cmd.py +90 -0
  30. mcpywrap_core/commands/init_cmd.py +48 -0
  31. mcpywrap_core/commands/mod_cmd.py +53 -0
  32. mcpywrap_core/commands/modsdk_cmd.py +41 -0
  33. mcpywrap_core/commands/package_cmd.py +16 -0
  34. mcpywrap_core/commands/publish_cmd.py +51 -0
  35. mcpywrap_core/commands/record_cmd.py +85 -0
  36. mcpywrap_core/commands/remove_cmd.py +62 -0
  37. mcpywrap_core/commands/run_cmd.py +169 -0
  38. mcpywrap_core/commands/runtime_cmd.py +150 -0
  39. mcpywrap_core/commands/runtime_input_cmd.py +86 -0
  40. mcpywrap_core/commands/runtime_player_cmd.py +192 -0
  41. mcpywrap_core/commands/runtime_ui_cmd.py +121 -0
  42. mcpywrap_core/commands/script_cmd.py +54 -0
  43. mcpywrap_core/commands/serve_cmd.py +39 -0
  44. mcpywrap_core/commands/session_cmd.py +95 -0
  45. mcpywrap_core/commands/sync_cmd.py +29 -0
  46. mcpywrap_core/commands/ui_cmd.py +36 -0
  47. mcpywrap_core/config.py +254 -0
  48. mcpywrap_core/consumer_prepare.py +471 -0
  49. mcpywrap_core/core-contract.json +6 -0
  50. mcpywrap_core/custom_packaging.py +41 -0
  51. mcpywrap_core/dependencies.py +326 -0
  52. mcpywrap_core/engines/__init__.py +1 -0
  53. mcpywrap_core/engines/android_icon.py +186 -0
  54. mcpywrap_core/engines/apk_profile.py +90 -0
  55. mcpywrap_core/engines/backend.py +134 -0
  56. mcpywrap_core/engines/host.py +32 -0
  57. mcpywrap_core/engines/install.py +665 -0
  58. mcpywrap_core/engines/macos.py +483 -0
  59. mcpywrap_core/engines/macos_icons.py +159 -0
  60. mcpywrap_core/engines/macos_network.py +58 -0
  61. mcpywrap_core/engines/official.py +129 -0
  62. mcpywrap_core/engines/preferences.py +146 -0
  63. mcpywrap_core/engines/tui.py +110 -0
  64. mcpywrap_core/engines/windows.py +144 -0
  65. mcpywrap_core/framework_presets.py +22 -0
  66. mcpywrap_core/frameworks.py +77 -0
  67. mcpywrap_core/git_cache.py +154 -0
  68. mcpywrap_core/git_projects.py +266 -0
  69. mcpywrap_core/host.py +93 -0
  70. mcpywrap_core/host_assets.py +14 -0
  71. mcpywrap_core/input_executor.py +246 -0
  72. mcpywrap_core/input_plan.py +257 -0
  73. mcpywrap_core/mcstudio/SimpleMonitor.py +51 -0
  74. mcpywrap_core/mcstudio/__init__.py +1 -0
  75. mcpywrap_core/mcstudio/auth_interaction.py +88 -0
  76. mcpywrap_core/mcstudio/bridge_assets.py +85 -0
  77. mcpywrap_core/mcstudio/deployment_links.py +670 -0
  78. mcpywrap_core/mcstudio/diagnostics.py +21 -0
  79. mcpywrap_core/mcstudio/discovery.py +337 -0
  80. mcpywrap_core/mcstudio/editor.py +81 -0
  81. mcpywrap_core/mcstudio/file_logs.py +66 -0
  82. mcpywrap_core/mcstudio/game.py +256 -0
  83. mcpywrap_core/mcstudio/host_input.py +252 -0
  84. mcpywrap_core/mcstudio/hot_reload.py +218 -0
  85. mcpywrap_core/mcstudio/image.py +20 -0
  86. mcpywrap_core/mcstudio/input_sequence.py +204 -0
  87. mcpywrap_core/mcstudio/launcher_control.py +56 -0
  88. mcpywrap_core/mcstudio/launcher_python.py +134 -0
  89. mcpywrap_core/mcstudio/log_colorizer.py +252 -0
  90. mcpywrap_core/mcstudio/log_protocol.py +74 -0
  91. mcpywrap_core/mcstudio/log_stream.py +136 -0
  92. mcpywrap_core/mcstudio/mcs.py +149 -0
  93. mcpywrap_core/mcstudio/mcs_auth.py +196 -0
  94. mcpywrap_core/mcstudio/network.py +187 -0
  95. mcpywrap_core/mcstudio/private_logs.py +70 -0
  96. mcpywrap_core/mcstudio/processes.py +41 -0
  97. mcpywrap_core/mcstudio/recording_worker.py +154 -0
  98. mcpywrap_core/mcstudio/recordings.py +351 -0
  99. mcpywrap_core/mcstudio/runtime_cppconfig.py +249 -0
  100. mcpywrap_core/mcstudio/runtime_debug.py +437 -0
  101. mcpywrap_core/mcstudio/runtime_input_payload.py +277 -0
  102. mcpywrap_core/mcstudio/runtime_key_timeline_payload.py +229 -0
  103. mcpywrap_core/mcstudio/runtime_player_payload.py +713 -0
  104. mcpywrap_core/mcstudio/runtime_ui.py +212 -0
  105. mcpywrap_core/mcstudio/runtime_ui_outline.py +424 -0
  106. mcpywrap_core/mcstudio/runtime_ui_payload.py +560 -0
  107. mcpywrap_core/mcstudio/screenshot.py +94 -0
  108. mcpywrap_core/mcstudio/session_capabilities.py +54 -0
  109. mcpywrap_core/mcstudio/session_worker.py +177 -0
  110. mcpywrap_core/mcstudio/sessions.py +285 -0
  111. mcpywrap_core/mcstudio/studio_server.py +363 -0
  112. mcpywrap_core/mcstudio/studio_server_ui.py +126 -0
  113. mcpywrap_core/mcstudio/symlink_helper_global.py +75 -0
  114. mcpywrap_core/mcstudio/symlink_helper_map.py +114 -0
  115. mcpywrap_core/mcstudio/symlinks.py +534 -0
  116. mcpywrap_core/mcstudio/unified_input.py +39 -0
  117. mcpywrap_core/mcstudio/window.py +755 -0
  118. mcpywrap_core/minecraft/__init__.py +1 -0
  119. mcpywrap_core/minecraft/addons.py +148 -0
  120. mcpywrap_core/minecraft/level_dat.py +958 -0
  121. mcpywrap_core/minecraft/map.py +85 -0
  122. mcpywrap_core/minecraft/modsdk_environment.py +197 -0
  123. mcpywrap_core/minecraft/netease_modsdk.py +269 -0
  124. mcpywrap_core/minecraft/template/generate_mod_files.py +116 -0
  125. mcpywrap_core/minecraft/template/mod_template.py +494 -0
  126. mcpywrap_core/operations/__init__.py +1 -0
  127. mcpywrap_core/operations/build.py +66 -0
  128. mcpywrap_core/operations/package.py +97 -0
  129. mcpywrap_core/operations/sync.py +82 -0
  130. mcpywrap_core/prepare_environment.py +149 -0
  131. mcpywrap_core/project_dependencies.py +164 -0
  132. mcpywrap_core/project_init.py +73 -0
  133. mcpywrap_core/project_scripts.py +369 -0
  134. mcpywrap_core/recording_io.py +122 -0
  135. mcpywrap_core/remote/__init__.py +1 -0
  136. mcpywrap_core/remote/client.py +271 -0
  137. mcpywrap_core/remote/http_server.py +205 -0
  138. mcpywrap_core/remote/protocol.py +22 -0
  139. mcpywrap_core/remote/service.py +317 -0
  140. mcpywrap_core/runtime/__init__.py +1 -0
  141. mcpywrap_core/runtime/deployment.py +501 -0
  142. mcpywrap_core/runtime/deployment_inspection.py +645 -0
  143. mcpywrap_core/runtime/deployment_service.py +688 -0
  144. mcpywrap_core/runtime/diagnostics.py +74 -0
  145. mcpywrap_core/runtime/groups.py +666 -0
  146. mcpywrap_core/runtime/instances.py +147 -0
  147. mcpywrap_core/runtime/native.py +64 -0
  148. mcpywrap_core/runtime/occupancy.py +78 -0
  149. mcpywrap_core/runtime/preparation.py +97 -0
  150. mcpywrap_core/runtime/profiles.py +183 -0
  151. mcpywrap_core/runtime/storage.py +42 -0
  152. mcpywrap_core/runtime/targets.py +24 -0
  153. mcpywrap_core/runtime/watch.py +158 -0
  154. mcpywrap_core/runtime/windows.py +184 -0
  155. mcpywrap_core/source_files.py +82 -0
  156. mcpywrap_core/timeline.py +26 -0
  157. mcpywrap_core/ui/__init__.py +5 -0
  158. mcpywrap_core/ui/application.py +58 -0
  159. mcpywrap_core/ui/assets/mcpy.icns +0 -0
  160. mcpywrap_core/ui/assets/mcpy.icon/Assets/01-cube.svg +5 -0
  161. mcpywrap_core/ui/assets/mcpy.icon/Assets/02-prompt.svg +4 -0
  162. mcpywrap_core/ui/assets/mcpy.icon/icon.json +56 -0
  163. mcpywrap_core/ui/assets/mcpy.png +0 -0
  164. mcpywrap_core/ui/engine_setup.py +119 -0
  165. mcpywrap_core/ui/log_view.py +201 -0
  166. mcpywrap_core/ui/log_window.py +147 -0
  167. mcpywrap_core/ui/project_ui.py +963 -0
  168. mcpywrap_core/ui/run_session.py +89 -0
  169. mcpywrap_core/ui/session_controller.py +416 -0
  170. mcpywrap_core/ui/session_tools.py +76 -0
  171. mcpywrap_core/ui/session_views.py +236 -0
  172. mcpywrap_core/ui/world_config.py +112 -0
  173. mcpywrap_core/utils/__init__.py +6 -0
  174. mcpywrap_core/utils/console.py +13 -0
  175. mcpywrap_core/utils/link_snapshot.py +131 -0
  176. mcpywrap_core/utils/locks.py +85 -0
  177. mcpywrap_core/utils/paths.py +18 -0
  178. mcpywrap_core/utils/pip_error_parser.py +184 -0
  179. mcpywrap_core/utils/print_guide.py +30 -0
  180. mcpywrap_core/utils/project_setup.py +148 -0
  181. mcpywrap_core/utils/utils.py +38 -0
  182. mcpywrap_core-0.6.0.dist-info/METADATA +33 -0
  183. mcpywrap_core-0.6.0.dist-info/RECORD +189 -0
  184. mcpywrap_core-0.6.0.dist-info/WHEEL +5 -0
  185. mcpywrap_core-0.6.0.dist-info/licenses/LICENSE +21 -0
  186. mcpywrap_core-0.6.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +35 -0
  187. mcpywrap_core-0.6.0.dist-info/top_level.txt +2 -0
  188. mcpywrap_host/__init__.py +9 -0
  189. mcpywrap_host/store.py +276 -0
@@ -0,0 +1,2 @@
1
+ """Shared mcpy engineering implementation."""
2
+ from ._version import __version__
@@ -0,0 +1,18 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ mcpywrap 命令行工具的入口点
4
+ """
5
+
6
+ from .cli import cli
7
+
8
+ def main():
9
+ """主函数,作为 CLI 入口点"""
10
+ import sys
11
+ # Keep the caller's text encoding; unsupported symbols must not abort an operation.
12
+ for stream in (sys.stdout, sys.stderr):
13
+ if hasattr(stream, 'reconfigure'):
14
+ stream.reconfigure(errors='backslashreplace')
15
+ cli()
16
+
17
+ if __name__ == "__main__":
18
+ main()
@@ -0,0 +1,278 @@
1
+ """Private finite-script supervisor. Invoked by absolute path; never a public CLI."""
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ import signal
6
+ import subprocess
7
+ import sys
8
+ import time
9
+
10
+ import psutil
11
+
12
+
13
+ def identity(pid):
14
+ p = psutil.Process(pid)
15
+ return {'pid': pid, 'created_at': p.create_time(), 'executable': p.exe()}
16
+
17
+
18
+ def alive(record):
19
+ try:
20
+ p = psutil.Process(record['pid'])
21
+ return abs(p.create_time() - record['created_at']) < .01 and p.status() != psutil.STATUS_ZOMBIE
22
+ except psutil.NoSuchProcess:
23
+ return False
24
+
25
+
26
+ def save(path, data):
27
+ temporary = path.with_suffix('.writing')
28
+ temporary.write_text(json.dumps(data, ensure_ascii=True), encoding='utf-8')
29
+ for attempt in range(10):
30
+ try:
31
+ temporary.replace(path)
32
+ return
33
+ except PermissionError:
34
+ if os.name != 'nt' or attempt == 9: raise
35
+ time.sleep(.02)
36
+
37
+
38
+ class WindowsJob:
39
+ """Closing the supervisor's handle terminates its assigned command tree."""
40
+ def __init__(self):
41
+ import ctypes as c
42
+ from ctypes import wintypes as w
43
+ class Basic(c.Structure):
44
+ _fields_ = [('PerProcessUserTimeLimit', c.c_longlong), ('PerJobUserTimeLimit', c.c_longlong),
45
+ ('LimitFlags', w.DWORD), ('MinimumWorkingSetSize', c.c_size_t),
46
+ ('MaximumWorkingSetSize', c.c_size_t), ('ActiveProcessLimit', w.DWORD),
47
+ ('Affinity', c.c_size_t), ('PriorityClass', w.DWORD), ('SchedulingClass', w.DWORD)]
48
+ class IO(c.Structure):
49
+ _fields_ = [(name, c.c_ulonglong) for name in ('ReadOperationCount', 'WriteOperationCount',
50
+ 'OtherOperationCount', 'ReadTransferCount', 'WriteTransferCount', 'OtherTransferCount')]
51
+ class Extended(c.Structure):
52
+ _fields_ = [('BasicLimitInformation', Basic), ('IoInfo', IO),
53
+ ('ProcessMemoryLimit', c.c_size_t), ('JobMemoryLimit', c.c_size_t),
54
+ ('PeakProcessMemoryUsed', c.c_size_t), ('PeakJobMemoryUsed', c.c_size_t)]
55
+ self.kernel = c.WinDLL('kernel32', use_last_error=True)
56
+ self.kernel.CreateJobObjectW.argtypes = [c.c_void_p, w.LPCWSTR]
57
+ self.kernel.CreateJobObjectW.restype = w.HANDLE
58
+ self.kernel.SetInformationJobObject.argtypes = [w.HANDLE, c.c_int, c.c_void_p, w.DWORD]
59
+ self.kernel.AssignProcessToJobObject.argtypes = [w.HANDLE, w.HANDLE]
60
+ self.kernel.QueryInformationJobObject.argtypes = [w.HANDLE, c.c_int, c.c_void_p, w.DWORD, c.c_void_p]
61
+ self.kernel.TerminateJobObject.argtypes = [w.HANDLE, w.UINT]
62
+ self.kernel.OpenProcess.argtypes = [w.DWORD, w.BOOL, w.DWORD]
63
+ self.kernel.OpenProcess.restype = w.HANDLE
64
+ self.kernel.IsProcessInJob.argtypes = [w.HANDLE, w.HANDLE, c.POINTER(w.BOOL)]
65
+ self.kernel.WaitForSingleObject.argtypes = [w.HANDLE, w.DWORD]
66
+ self.kernel.WaitForSingleObject.restype = w.DWORD
67
+ self.kernel.CloseHandle.argtypes = [w.HANDLE]
68
+ self.kernel.OpenThread.argtypes = [w.DWORD, w.BOOL, w.DWORD]
69
+ self.kernel.OpenThread.restype = w.HANDLE
70
+ self.kernel.ResumeThread.argtypes = [w.HANDLE]
71
+ self.kernel.ResumeThread.restype = w.DWORD
72
+ self.handle = self.kernel.CreateJobObjectW(None, None)
73
+ if not self.handle: raise c.WinError(c.get_last_error())
74
+ info = Extended(); info.BasicLimitInformation.LimitFlags = 0x2000 # KILL_ON_JOB_CLOSE
75
+ if not self.kernel.SetInformationJobObject(self.handle, 9, c.byref(info), c.sizeof(info)):
76
+ error = c.WinError(c.get_last_error()); self.close(); raise error
77
+
78
+ def assign(self, process):
79
+ import ctypes as c
80
+ if not self.kernel.AssignProcessToJobObject(self.handle, int(process._handle)):
81
+ raise c.WinError(c.get_last_error())
82
+ # The command was created suspended: no child can escape before job assignment.
83
+ threads = psutil.Process(process.pid).threads()
84
+ if not threads:
85
+ raise RuntimeError('脚本主线程不存在')
86
+ thread = self.kernel.OpenThread(0x0002, False, threads[0].id)
87
+ if not thread:
88
+ raise c.WinError(c.get_last_error())
89
+ try:
90
+ if self.kernel.ResumeThread(thread) == 0xffffffff:
91
+ raise c.WinError(c.get_last_error())
92
+ finally:
93
+ self.kernel.CloseHandle(thread)
94
+
95
+ def close(self):
96
+ if self.handle:
97
+ self.kernel.CloseHandle(self.handle)
98
+ self.handle = None
99
+
100
+ def process_ids(self):
101
+ import ctypes as c
102
+ from ctypes import wintypes as w
103
+ capacity = 16
104
+ for _ in range(8):
105
+ class ProcessIds(c.Structure):
106
+ _fields_ = [('assigned', w.DWORD), ('listed', w.DWORD),
107
+ ('ids', c.c_size_t * capacity)]
108
+ info = ProcessIds()
109
+ # JobObjectBasicProcessIdList retains descendants after their
110
+ # parent exits. A concurrent spawn may require a larger buffer.
111
+ if self.kernel.QueryInformationJobObject(self.handle, 3, c.byref(info), c.sizeof(info), None):
112
+ return list(info.ids[:info.listed])
113
+ error = c.get_last_error()
114
+ if error != 234: # ERROR_MORE_DATA
115
+ raise c.WinError(error)
116
+ capacity = max(capacity * 2, info.assigned)
117
+ raise RuntimeError('脚本进程树持续增长,无法确认成员')
118
+
119
+ def active_count(self):
120
+ import ctypes as c
121
+ from ctypes import wintypes as w
122
+ count = 0
123
+ for pid in self.process_ids():
124
+ handle = self.kernel.OpenProcess(0x00100000 | 0x1000, False, pid) # SYNCHRONIZE | QUERY_LIMITED_INFORMATION
125
+ if not handle:
126
+ error = c.get_last_error()
127
+ if error == 87: # ERROR_INVALID_PARAMETER: process already gone
128
+ continue
129
+ raise c.WinError(error)
130
+ try:
131
+ belongs = w.BOOL()
132
+ if not self.kernel.IsProcessInJob(handle, self.handle, c.byref(belongs)):
133
+ raise c.WinError(c.get_last_error())
134
+ if not belongs.value:
135
+ continue # The PID may have been reused outside this job.
136
+ state = self.kernel.WaitForSingleObject(handle, 0)
137
+ if state == 258: # WAIT_TIMEOUT: process is still running
138
+ count += 1
139
+ elif state != 0: # WAIT_OBJECT_0: process has exited
140
+ raise c.WinError(c.get_last_error())
141
+ finally:
142
+ self.kernel.CloseHandle(handle)
143
+ return count
144
+
145
+ def terminate(self):
146
+ import ctypes as c
147
+ if not self.kernel.TerminateJobObject(self.handle, 1):
148
+ raise c.WinError(c.get_last_error())
149
+
150
+
151
+ def members(process, known):
152
+ try:
153
+ for p in psutil.Process(process.pid).children(recursive=True):
154
+ known[p.pid] = identity(p.pid)
155
+ except psutil.NoSuchProcess:
156
+ pass
157
+ if os.name != 'nt':
158
+ for p in psutil.process_iter(['pid', 'status']):
159
+ try:
160
+ if p.info['status'] != psutil.STATUS_ZOMBIE and os.getpgid(p.pid) == process.pid:
161
+ known[p.pid] = identity(p.pid)
162
+ except (ProcessLookupError, PermissionError, psutil.NoSuchProcess):
163
+ pass
164
+ return [r for r in known.values() if alive(r)]
165
+
166
+
167
+ def finish_tree(process, known, job):
168
+ if job:
169
+ # Keep the job handle until exit is verified, including descendants
170
+ # never observed by psutil. Closing it alone only requests termination.
171
+ job.terminate()
172
+ if process.poll() is None:
173
+ process.kill() # Also handles failure to assign a suspended child.
174
+ process.wait(timeout=3)
175
+ deadline = time.monotonic() + 2
176
+ while job.active_count():
177
+ if time.monotonic() >= deadline:
178
+ raise RuntimeError('脚本后代进程尚未确认退出')
179
+ time.sleep(.05)
180
+ return
181
+ if os.name != 'nt':
182
+ try: os.killpg(process.pid, signal.SIGTERM)
183
+ except ProcessLookupError: pass
184
+ pending = members(process, known)
185
+ for record in pending:
186
+ try: psutil.Process(record['pid']).terminate()
187
+ except psutil.NoSuchProcess: pass
188
+ deadline = time.monotonic() + 2
189
+ while time.monotonic() < deadline and any(alive(r) for r in pending):
190
+ process.poll()
191
+ time.sleep(.05)
192
+ remaining = members(process, known)
193
+ if not remaining:
194
+ process.wait(timeout=3)
195
+ return
196
+ if os.name != 'nt':
197
+ try: os.killpg(process.pid, signal.SIGKILL)
198
+ except ProcessLookupError: pass
199
+ for record in remaining:
200
+ if alive(record):
201
+ try: psutil.Process(record['pid']).kill()
202
+ except psutil.NoSuchProcess: pass
203
+ process.wait(timeout=3)
204
+ deadline = time.monotonic() + 2
205
+ while time.monotonic() < deadline:
206
+ if not any(alive(r) for r in known.values()): return
207
+ time.sleep(.05)
208
+ raise RuntimeError('脚本后代进程尚未确认退出')
209
+
210
+
211
+ def main():
212
+ spec = json.loads(sys.stdin.buffer.read())
213
+ path = Path(spec.pop('result'))
214
+ command, timeout = spec.pop('command'), spec.pop('timeout')
215
+ cancel, parent = Path(spec.pop('cancel')), spec.pop('parent')
216
+ state = dict(spec, supervisor=identity(os.getpid()), state='starting')
217
+ process, job, known = None, None, {}
218
+ started = time.monotonic()
219
+ try:
220
+ save(path, state)
221
+ if not alive(parent) or cancel.exists():
222
+ state.update(state='cancelled', code='script_cancelled')
223
+ return
224
+ if os.name == 'nt': job = WindowsJob()
225
+ state['state'] = 'spawning'; save(path, state)
226
+ process = subprocess.Popen(command, stdin=subprocess.DEVNULL,
227
+ **({'creationflags': subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP | 0x00000004}
228
+ if os.name == 'nt' else {'start_new_session': True}))
229
+ try:
230
+ child = identity(process.pid)
231
+ except psutil.ZombieProcess:
232
+ child = {'pid': process.pid, 'created_at': psutil.Process(process.pid).create_time(), 'executable': command[0]}
233
+ state.update(child=child, group=process.pid if os.name != 'nt' else None, state='running')
234
+ known[process.pid] = child
235
+ save(path, state)
236
+ if job: job.assign(process)
237
+ while process.poll() is None:
238
+ if not job:
239
+ members(process, known)
240
+ if cancel.exists() or not alive(parent):
241
+ state.update(state='cancelled', code='script_cancelled', error='操作已取消或调用进程已退出')
242
+ break
243
+ if time.monotonic() - started >= timeout:
244
+ state.update(state='timed_out', code='script_timeout', error='执行超时')
245
+ break
246
+ time.sleep(.05)
247
+ else:
248
+ # Windows 的 Job 是所有权依据。父进程退出后再按 PID 查询树,
249
+ # 可能观察到复用该 PID 的无关进程;同时给短暂的退出收尾留出时间。
250
+ descendants = [] if job else [r for r in members(process, known) if r['pid'] != process.pid]
251
+ remaining = job.active_count() if job else 0
252
+ settle_deadline = time.monotonic() + .2
253
+ while remaining and time.monotonic() < settle_deadline:
254
+ time.sleep(.01)
255
+ remaining = job.active_count()
256
+ state.update(state='completed' if process.returncode == 0 else 'failed',
257
+ code=None if process.returncode == 0 else 'script_failed', exit_code=process.returncode)
258
+ if descendants or remaining:
259
+ state.update(state='failed', code='script_background_process', error='有限任务不能遗留后台进程')
260
+ except BaseException as exc:
261
+ state.update(state='failed', code='script_start_failed', error=str(exc))
262
+ finally:
263
+ if process:
264
+ try:
265
+ finish_tree(process, known, job)
266
+ except BaseException as exc:
267
+ import traceback
268
+ traceback.print_exc()
269
+ state.update(state='recovery_required', code='script_recovery_required', error=str(exc))
270
+ state.setdefault('exit_code', process.returncode)
271
+ if job: job.close()
272
+ state['duration'] = time.monotonic() - started
273
+ state['finished_at'] = time.time()
274
+ save(path, state)
275
+
276
+
277
+ if __name__ == '__main__':
278
+ main()
@@ -0,0 +1 @@
1
+ __version__ = "0.6.0"
mcpywrap_core/api.py ADDED
@@ -0,0 +1,69 @@
1
+ """Versioned engineering API. All frontends call the same operation functions."""
2
+ from pathlib import Path
3
+
4
+ from . import __version__
5
+ from .host import HOST_API, HostServices, host_scope, current_host
6
+
7
+ CORE_API = 1
8
+ OPERATIONS = ('resolve', 'build', 'package', 'sync')
9
+
10
+
11
+ class Core:
12
+ def __init__(self, host=None):
13
+ self.host = host or current_host()
14
+
15
+ def describe(self):
16
+ return {'core_api': CORE_API, 'host_api': HOST_API, 'version': __version__,
17
+ 'operations': list(OPERATIONS), 'capabilities': self.host.capabilities()}
18
+
19
+ def execute(self, operation, project, **options):
20
+ """Execute or raise the original structured error; never invoke another core."""
21
+ if operation not in OPERATIONS:
22
+ raise ValueError('未知工程操作:' + str(operation))
23
+ allowed = {'resolve': set(), 'build': {'merge'}, 'package': {'merge'},
24
+ 'sync': {'install', 'migrate_windows_lock'}}[operation]
25
+ if set(options) - allowed or any(type(value) is not bool for value in options.values()):
26
+ raise ValueError('无效的工程操作参数')
27
+ project = Path(project).expanduser().resolve()
28
+ if not project.is_dir() or not (project/'pyproject.toml').is_file():
29
+ raise ValueError('未找到项目 pyproject.toml:' + str(project))
30
+ from .command_context import project_scope
31
+ with host_scope(self.host), project_scope(project) as context:
32
+ if operation == 'resolve':
33
+ from .dependencies import DependencyService
34
+ manager = DependencyService(project).resolve()
35
+ result = {'project': str(project), 'dependencies': [
36
+ {'name': pack.pkg_name, 'path': str(pack.path)}
37
+ for pack in manager.get_all_dependencies().values()], 'warnings': manager.warnings}
38
+ elif operation == 'build':
39
+ from .operations.build import build_project
40
+ result = build_project(project, **options)
41
+ elif operation == 'package':
42
+ from .operations.package import package_project
43
+ result = package_project(project, **options)
44
+ else:
45
+ from .operations.sync import sync_project
46
+ result = sync_project(project, **options)
47
+ if context.warnings:
48
+ result['warnings'] = list(dict.fromkeys([*result.get('warnings', []), *context.warnings]))
49
+ return result
50
+
51
+ def request(self, request):
52
+ """Transport-neutral envelope. Authentication/path grants belong to the host.
53
+
54
+ The host resolves an authorized project ID to a path before calling this
55
+ method. Do not expose raw caller-supplied paths on an unauthenticated port.
56
+ """
57
+ try:
58
+ if not isinstance(request, dict) or request.get('core_api') != CORE_API:
59
+ raise ValueError('不兼容的核心协议')
60
+ if request.get('operation') == 'describe':
61
+ return {'ok': True, 'result': self.describe()}
62
+ options = request.get('options', {})
63
+ if not isinstance(options, dict) or not isinstance(request.get('project'), str):
64
+ raise ValueError('无效的工程请求')
65
+ result = self.execute(request.get('operation'), request['project'], **options)
66
+ return {'ok': True, 'result': result, 'core': self.describe()}
67
+ except (ValueError, OSError) as error:
68
+ return {'ok': False, 'error': str(error), 'code': getattr(error, 'code', 'operation_failed'),
69
+ 'hint': getattr(error, 'hint', None), 'details': getattr(error, 'details', {})}
@@ -0,0 +1,310 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import os
4
+ import shutil
5
+ import click
6
+
7
+ from ..utils.utils import ensure_dir
8
+ from .file_merge import try_merge_file
9
+
10
+ # Python 包管理和其他应该忽略的文件和目录
11
+ EXCLUDED_PATTERNS = [
12
+ # Python 包管理
13
+ ".egg-info",
14
+ "__pycache__",
15
+ ".pyc",
16
+ ".pyo",
17
+ ".pyd",
18
+ ".so",
19
+ ".dll",
20
+ ".eggs",
21
+ ".pytest_cache",
22
+ ".tox",
23
+ ".coverage",
24
+ ".coverage.*",
25
+ "htmlcov",
26
+ # 版本控制
27
+ ".git",
28
+ ".hg",
29
+ ".svn",
30
+ ".bzr",
31
+ # 其他临时文件
32
+ ".DS_Store",
33
+ "Thumbs.db"
34
+ ]
35
+
36
+ MANIFEST_FILES = [
37
+ "manifest.json",
38
+ "pack_manifest.json"
39
+ ]
40
+
41
+
42
+ class AddonsPack(object):
43
+
44
+ pkg_name: str
45
+ path: str
46
+ is_origin: bool
47
+ behavior_pack_dir: str
48
+ resource_pack_dir: str
49
+
50
+ def __init__(self, pkg_name, path, is_origin=False):
51
+ self.pkg_name = pkg_name
52
+ self.path = os.path.abspath(path)
53
+ self.is_origin = is_origin
54
+ self.behavior_pack_dir = None
55
+ self.resource_pack_dir = None
56
+ # 进入此目录,查找内部的行为包和资源包的路径
57
+ for item in os.listdir(self.path):
58
+ item_path = os.path.join(self.path, item)
59
+ if os.path.isdir(item_path):
60
+ if item.startswith("behavior_pack") or item.startswith("BehaviorPack"):
61
+ self.behavior_pack_dir = item_path
62
+ elif item.startswith("resource_pack") or item.startswith("ResourcePack"):
63
+ self.resource_pack_dir = item_path
64
+ if not self.behavior_pack_dir:
65
+ self.behavior_pack_dir = os.path.join(self.path, "behavior_pack")
66
+ if not self.resource_pack_dir:
67
+ self.resource_pack_dir = os.path.join(self.path, "resource_pack")
68
+
69
+ def should_exclude(self, path):
70
+ """判断文件或目录是否应该被排除"""
71
+ # Only inspect components inside this pack. A checkout named demo.git-copy
72
+ # or a perfectly valid terrain.soil.json is not an excluded artifact.
73
+ from pathlib import Path
74
+ if os.name == 'nt':
75
+ # Python 3.9 treats C: and \\?\C: as different relpath mounts.
76
+ # Cache paths use the extended spelling; resolved files may not.
77
+ from ..source_files import long_path
78
+ relative = os.path.relpath(long_path(path), long_path(self.path))
79
+ else:
80
+ relative = os.path.relpath(path, self.path)
81
+ parts = Path(relative).parts
82
+ directories = {'.git', '.hg', '.svn', '.eggs', '.pytest_cache', '.tox',
83
+ '.bzr', '__pycache__', 'htmlcov'}
84
+ if any(part in directories or part.endswith('.egg-info') for part in parts):
85
+ return True
86
+ name = parts[-1] if parts else ''
87
+ if (name in ('.DS_Store', 'Thumbs.db', '.coverage') or name.startswith('.coverage.')
88
+ or Path(name).suffix.lower() in ('.pyc', '.pyo', '.pyd', '.so', '.dll')):
89
+ return True
90
+ # 得到文件名
91
+ filename = os.path.basename(path)
92
+ if not self.is_origin and filename in MANIFEST_FILES:
93
+ return True
94
+ return False
95
+
96
+ def copy_behavior_to(self, target_dir: str, rename: str = None):
97
+ """
98
+ 复制行为包和资源包到目标目录
99
+ target_dir为父目录
100
+ """
101
+ if self.behavior_pack_dir:
102
+ target_path = os.path.join(target_dir, os.path.basename(self.behavior_pack_dir) if not rename else rename)
103
+ os.makedirs(target_path, exist_ok=True)
104
+
105
+ # 使用自定义复制函数而不是shutil.copytree
106
+ for root, dirs, files in os.walk(self.behavior_pack_dir):
107
+ # 过滤掉应该排除的目录
108
+ dirs[:] = [d for d in dirs if not self.should_exclude(os.path.join(root, d))]
109
+
110
+ # 计算相对路径
111
+ rel_path = os.path.relpath(root, self.behavior_pack_dir)
112
+ # 计算目标目录
113
+ target_root = os.path.join(target_path, rel_path) if rel_path != '.' else target_path
114
+ ensure_dir(target_root)
115
+
116
+ # 复制文件
117
+ for file in files:
118
+ src_file = os.path.join(root, file)
119
+ if not self.should_exclude(src_file):
120
+ dest_file = os.path.join(target_root, file)
121
+ # 如果是Python文件,检查并添加编码声明
122
+ if file.endswith('.py'):
123
+ self._copy_with_encoding_check(src_file, dest_file)
124
+ else:
125
+ shutil.copy2(src_file, dest_file)
126
+
127
+ def copy_resource_to(self, target_dir: str, rename: str = None):
128
+ """
129
+ 复制资源包到目标目录
130
+ target_dir为父目录
131
+ """
132
+ if self.resource_pack_dir:
133
+ target_path = os.path.join(target_dir, os.path.basename(self.resource_pack_dir) if not rename else rename)
134
+ os.makedirs(target_path, exist_ok=True)
135
+
136
+ # 使用自定义复制函数而不是shutil.copytree
137
+ for root, dirs, files in os.walk(self.resource_pack_dir):
138
+ # 过滤掉应该排除的目录
139
+ dirs[:] = [d for d in dirs if not self.should_exclude(os.path.join(root, d))]
140
+
141
+ # 计算相对路径
142
+ rel_path = os.path.relpath(root, self.resource_pack_dir)
143
+ # 计算目标目录
144
+ target_root = os.path.join(target_path, rel_path) if rel_path != '.' else target_path
145
+ ensure_dir(target_root)
146
+
147
+ # 复制文件
148
+ for file in files:
149
+ src_file = os.path.join(root, file)
150
+ if not self.should_exclude(src_file):
151
+ dest_file = os.path.join(target_root, file)
152
+ # 如果是Python文件,检查并添加编码声明
153
+ if file.endswith('.py'):
154
+ self._copy_with_encoding_check(src_file, dest_file)
155
+ else:
156
+ shutil.copy2(src_file, dest_file)
157
+
158
+ def merge_behavior_into(self, target_behavior_dir: str):
159
+ """合并行为包到目标行为包目录"""
160
+ if self.behavior_pack_dir:
161
+ for root, dirs, files in os.walk(self.behavior_pack_dir):
162
+ # 过滤掉应该排除的目录
163
+ dirs[:] = [d for d in dirs if not self.should_exclude(os.path.join(root, d))]
164
+
165
+ # 计算相对路径
166
+ rel_path = os.path.relpath(root, self.behavior_pack_dir)
167
+ # 计算目标目录
168
+ target_root = os.path.join(target_behavior_dir, rel_path) if rel_path != '.' else target_behavior_dir
169
+ ensure_dir(target_root)
170
+
171
+ # 复制文件
172
+ for file in files:
173
+ src_file = os.path.join(root, file)
174
+ if not self.should_exclude(src_file):
175
+ # 计算源文件的相对路径
176
+ src_rel_path = os.path.relpath(src_file, self.behavior_pack_dir)
177
+ dest_file = os.path.join(target_root, file)
178
+ self.merge_behavior_single_file_to(src_rel_path, dest_file)
179
+
180
+ def merge_resource_into(self, target_resource_dir: str):
181
+ """合并资源包到目标资源包目录"""
182
+ if self.resource_pack_dir:
183
+ for root, dirs, files in os.walk(self.resource_pack_dir):
184
+ # 过滤掉应该排除的目录
185
+ dirs[:] = [d for d in dirs if not self.should_exclude(os.path.join(root, d))]
186
+
187
+ # 计算相对路径
188
+ rel_path = os.path.relpath(root, self.resource_pack_dir)
189
+ # 计算目标目录
190
+ target_root = os.path.join(target_resource_dir, rel_path) if rel_path != '.' else target_resource_dir
191
+ ensure_dir(target_root)
192
+
193
+ # 复制文件
194
+ for file in files:
195
+ src_file = os.path.join(root, file)
196
+ if not self.should_exclude(src_file):
197
+ # 计算源文件的相对路径
198
+ src_rel_path = os.path.relpath(src_file, self.resource_pack_dir)
199
+ dest_file = os.path.join(target_root, file)
200
+ self.merge_resource_single_file_to(src_rel_path, dest_file)
201
+
202
+ def merge_behavior_single_file_to(self, src_rel_path: str, dest_abs_path: str):
203
+ """合并行为包单个文件,src为相对路径,dest为绝对路径"""
204
+ if self.behavior_pack_dir:
205
+ src_file = os.path.join(self.behavior_pack_dir, src_rel_path)
206
+ if os.path.exists(src_file):
207
+ self._merge_single_file(src_file, dest_abs_path)
208
+ return True
209
+ return False
210
+
211
+ def merge_resource_single_file_to(self, src_rel_path: str, dest_abs_path: str):
212
+ """合并资源包单个文件,src为相对路径,dest为绝对路径"""
213
+ if self.resource_pack_dir:
214
+ src_file = os.path.join(self.resource_pack_dir, src_rel_path)
215
+ if os.path.exists(src_file):
216
+ self._merge_single_file(src_file, dest_abs_path)
217
+ return True
218
+ return False
219
+
220
+ def _merge_single_file(self, src_file: str, dest_file: str):
221
+ """合并单个文件"""
222
+ if os.path.exists(dest_file):
223
+ if self.should_exclude(dest_file):
224
+ return
225
+ # 处理文件冲突
226
+ if src_file.endswith('.py'):
227
+ self._copy_with_encoding_check(src_file, dest_file)
228
+ elif os.path.exists(dest_file):
229
+ suc, reason = try_merge_file(src_file, dest_file)
230
+ if not suc:
231
+ click.secho(f"⚠️ 警告: 文件合并异常 {src_file} -> {dest_file} {reason}", fg="yellow")
232
+ else:
233
+ shutil.copy2(src_file, dest_file)
234
+
235
+ def _copy_with_encoding_check(self, src_file, dest_file):
236
+ """复制Python文件,并检查添加编码声明"""
237
+ try:
238
+ import re
239
+ from pathlib import Path
240
+ content = Path(src_file).read_bytes()
241
+ # Preserve ASCII, BOM and valid PEP 263 declarations byte-for-byte.
242
+ # Only undeclared non-ASCII UTF-8 needs Python 2's encoding header.
243
+ declared = any(re.search(br'^[ \t\f]*#.*?coding[:=][ \t]*[-\w.]+', line)
244
+ for line in content.splitlines()[:2])
245
+ if not declared and not content.startswith(b'\xef\xbb\xbf') and any(b >= 128 for b in content):
246
+ content.decode('utf-8')
247
+ newline = b'\r\n' if b'\r\n' in content else b'\n'
248
+ header = b'# -*- coding: utf-8 -*-' + newline
249
+ if content.startswith(b'#!') and b'\n' in content:
250
+ first, rest = content.split(b'\n', 1)
251
+ content = first + b'\n' + header + rest
252
+ else:
253
+ content = header + content
254
+ Path(dest_file).write_bytes(content)
255
+
256
+ # 复制文件元数据
257
+ shutil.copystat(src_file, dest_file)
258
+ except Exception as e:
259
+ click.secho(f"⚠️ 添加编码声明时出错: {src_file} -> {dest_file}: {str(e)}", fg="yellow")
260
+ # 如果出错,则直接复制
261
+ shutil.copy2(src_file, dest_file)
262
+
263
+ def is_behavior_file(self, file_path):
264
+ """
265
+ 判断文件是否为行为包文件
266
+
267
+ Args:
268
+ file_path: 文件路径
269
+
270
+ Returns:
271
+ bool: 如果是行为包文件返回True
272
+ """
273
+ return "behavior_pack" in file_path.lower() or "behaviorpack" in file_path.lower()
274
+
275
+ def is_resource_file(self, file_path):
276
+ """
277
+ 判断文件是否为资源包文件
278
+
279
+ Args:
280
+ file_path: 文件路径
281
+
282
+ Returns:
283
+ bool: 如果是资源包文件返回True
284
+ """
285
+ return "resource_pack" in file_path.lower() or "resourcepack" in file_path.lower()
286
+
287
+ def get_relative_path_in_pack(self, file_path):
288
+ """
289
+ 获取文件在包中的相对路径
290
+
291
+ Args:
292
+ file_path: 文件的绝对路径
293
+
294
+ Returns:
295
+ tuple: (是否为包文件, 包类型, 相对路径)
296
+ 包类型: "behavior" 或 "resource"
297
+ 相对路径: 在包中的相对路径,如果不在包中则为None
298
+ """
299
+ # Watchdog 可能返回短路径或目录链接路径;保留相对文件名大小写。
300
+ absolute = os.path.realpath(file_path)
301
+ for kind, folder in (("behavior", self.behavior_pack_dir), ("resource", self.resource_pack_dir)):
302
+ if not folder:
303
+ continue
304
+ folder = os.path.realpath(folder)
305
+ try:
306
+ if os.path.commonpath([os.path.normcase(absolute), os.path.normcase(folder)]) == os.path.normcase(folder):
307
+ return True, kind, os.path.relpath(absolute, folder)
308
+ except ValueError:
309
+ continue
310
+ return False, None, None