@deepblend/dsh-blender-provider-local 0.2.9 → 0.2.11
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.
package/lib/index.js
CHANGED
|
@@ -363,6 +363,8 @@ class BlenderSession {
|
|
|
363
363
|
// itself, in the `ready` line bootstrap.py opens with — which is better evidence anyway, because
|
|
364
364
|
// it is the pid the script believes it is rather than the one the spawn returned.
|
|
365
365
|
this.pid = input.transport.pid ?? null
|
|
366
|
+
/** The workspace the other end says it serves, or null when it did not say. */
|
|
367
|
+
this.workspace = null
|
|
366
368
|
/** @type {Map<string, {resolve: Function, reject: Function, timer: object}>} */
|
|
367
369
|
this.waiting = new Map()
|
|
368
370
|
this.progress = []
|
|
@@ -456,6 +458,9 @@ class BlenderSession {
|
|
|
456
458
|
}
|
|
457
459
|
if (document?.kind === 'ready') {
|
|
458
460
|
if (typeof document.pid === 'number') this.pid = document.pid
|
|
461
|
+
// What the other end says it serves, when it says anything. Empty means "not stated", which the
|
|
462
|
+
// attach path treats as "serves anything" — every deployment before this key existed.
|
|
463
|
+
if (typeof document.workspace === 'string') this.workspace = document.workspace
|
|
459
464
|
this.readyResolve?.(this)
|
|
460
465
|
return
|
|
461
466
|
}
|
|
@@ -986,6 +991,24 @@ export default class LocalBlenderRuntime extends Service {
|
|
|
986
991
|
// connected and said nothing took 180 s to report — long enough that a user would conclude the
|
|
987
992
|
// product had hung.
|
|
988
993
|
await session.ready(ATTACH_READY_MS)
|
|
994
|
+
|
|
995
|
+
// THE WRONG-BLENDER CHECK. A socket path has a global default, so two workspaces on one machine
|
|
996
|
+
// can point the product at the SAME Blender — and without this, workspace B would drive the
|
|
997
|
+
// Blender workspace A's user is looking at, silently, because both sides behave correctly. A
|
|
998
|
+
// bridge that states a workspace and states a DIFFERENT one is refused; a bridge that states none
|
|
999
|
+
// is served, because that is what every deployment did before the key existed.
|
|
1000
|
+
const stated = session.workspace
|
|
1001
|
+
if (typeof stated === 'string' && stated.length > 0 && resolve(stated) !== resolve(this.workspaceRoot)) {
|
|
1002
|
+
await session.close()
|
|
1003
|
+
throw new BlenderError(
|
|
1004
|
+
BlenderErrorCode.RUNTIME_UNAVAILABLE,
|
|
1005
|
+
`The Blender at ${socketPath} serves the workspace ${stated}, and this deployment is ${this.workspaceRoot}. ` +
|
|
1006
|
+
'Point this deployment at its own bridge (DEEPBLEND_BRIDGE_SOCKET / sessionSocket), or start that Blender ' +
|
|
1007
|
+
'with DEEPBLEND_BRIDGE_WORKSPACE set to this workspace.',
|
|
1008
|
+
{ detail: { socketPath, bridgeWorkspace: stated, workspaceRoot: this.workspaceRoot } },
|
|
1009
|
+
)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
989
1012
|
return session
|
|
990
1013
|
}
|
|
991
1014
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-provider-local",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local Blender Provider: the BlenderRuntime Service over ctx.subprocess (batch execution).",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"./package.json": "./package.json"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@deepblend/dsh-blender-contracts": "0.2.
|
|
14
|
+
"@deepblend/dsh-blender-contracts": "0.2.11"
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|
|
Binary file
|
|
@@ -97,6 +97,15 @@ class _Bridge:
|
|
|
97
97
|
def __init__(self, socket_path, options=None):
|
|
98
98
|
self.socket_path = socket_path
|
|
99
99
|
self.options = options or {}
|
|
100
|
+
# WHICH WORKSPACE THIS BLENDER SERVES, when it was told. Empty means "not stated", which is
|
|
101
|
+
# what every deployment did before this existed — and the product treats "not stated" as
|
|
102
|
+
# "serves anything" rather than as a mismatch, so nothing that worked before stops working.
|
|
103
|
+
#
|
|
104
|
+
# WHY IT MATTERS: the socket path has a global default, so two workspaces on one machine can
|
|
105
|
+
# point the product at the SAME Blender. Without this, the product in workspace B would drive
|
|
106
|
+
# the Blender that workspace A's user is looking at, and the operations would land in a scene
|
|
107
|
+
# nobody intended — silently, because both sides are behaving correctly.
|
|
108
|
+
self.workspace = self.options.get("workspace") or ""
|
|
100
109
|
self.server = None
|
|
101
110
|
self.thread = None
|
|
102
111
|
self.requests = 0
|
|
@@ -131,6 +140,29 @@ class _Bridge:
|
|
|
131
140
|
self.thread = threading.Thread(target=self._serve, name="deepblend-bridge", daemon=True)
|
|
132
141
|
self.thread.start()
|
|
133
142
|
|
|
143
|
+
@staticmethod
|
|
144
|
+
def derive_workspace(open_file):
|
|
145
|
+
"""The workspace a Blender belongs to, read from the file it has open — or nothing at all.
|
|
146
|
+
|
|
147
|
+
WHY THIS IS A DERIVATION AND NOT A GUESS. A checkpoint the product wrote lives at
|
|
148
|
+
`<workspace>/.deepblend/projects/<id>/revisions/<rev>/scene.blend`, so a Blender that has one
|
|
149
|
+
of those open IS working in that workspace — the path says so, in the product's own layout.
|
|
150
|
+
Anything else (an empty path, a file the user made themselves, a project laid out differently)
|
|
151
|
+
returns nothing, and nothing is what the product treats as "serves anything".
|
|
152
|
+
|
|
153
|
+
THAT ASYMMETRY IS THE POINT: a bridge that states a workspace it cannot be sure of would be
|
|
154
|
+
refused by the product for a mismatch the user did not cause, and the fix would look like a
|
|
155
|
+
configuration problem when it is a wrong guess. Silence is the honest answer when the path does
|
|
156
|
+
not say.
|
|
157
|
+
"""
|
|
158
|
+
if not isinstance(open_file, str) or open_file == "":
|
|
159
|
+
return ""
|
|
160
|
+
marker = os.sep + ".deepblend" + os.sep
|
|
161
|
+
head, separator, _ = open_file.partition(marker)
|
|
162
|
+
if separator == "" or head == "":
|
|
163
|
+
return ""
|
|
164
|
+
return head
|
|
165
|
+
|
|
134
166
|
def _someone_is_listening(self):
|
|
135
167
|
"""Whether a live bridge is behind this path, rather than a file left by a dead one."""
|
|
136
168
|
probe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
@@ -187,7 +219,12 @@ class _Bridge:
|
|
|
187
219
|
nothing — so the product's `BlenderSession.ready()` waited forever on a peer that was already
|
|
188
220
|
serving. A transport that answers the same conversation has to open it the same way.
|
|
189
221
|
"""
|
|
190
|
-
connection.sendall((json.dumps({
|
|
222
|
+
connection.sendall((json.dumps({
|
|
223
|
+
"kind": "ready",
|
|
224
|
+
"pid": os.getpid(),
|
|
225
|
+
"socket": self.socket_path,
|
|
226
|
+
"workspace": self.workspace,
|
|
227
|
+
}) + "\n").encode("utf-8"))
|
|
191
228
|
buffer = b""
|
|
192
229
|
while True:
|
|
193
230
|
chunk = connection.recv(65536)
|
|
@@ -267,6 +304,8 @@ class DEEPBLEND_PT_bridge(bpy.types.Panel):
|
|
|
267
304
|
layout.label(text="Status: %s" % (_BRIDGE.status(),))
|
|
268
305
|
layout.label(text="Socket: %s" % (os.path.basename(_BRIDGE.socket_path),))
|
|
269
306
|
layout.label(text="Operations served: %d" % (_BRIDGE.requests,))
|
|
307
|
+
if _BRIDGE.workspace:
|
|
308
|
+
layout.label(text="Workspace: %s" % (os.path.basename(_BRIDGE.workspace),))
|
|
270
309
|
if _BRIDGE.last_action is not None:
|
|
271
310
|
layout.label(text="Last: %s" % (_BRIDGE.last_action,))
|
|
272
311
|
if _BRIDGE.last_error is not None:
|
|
@@ -276,7 +315,11 @@ class DEEPBLEND_PT_bridge(bpy.types.Panel):
|
|
|
276
315
|
def register():
|
|
277
316
|
global _BRIDGE
|
|
278
317
|
socket_path = os.environ.get("DEEPBLEND_BRIDGE_SOCKET", DEFAULT_SOCKET)
|
|
279
|
-
|
|
318
|
+
workspace = os.environ.get("DEEPBLEND_BRIDGE_WORKSPACE", "")
|
|
319
|
+
if workspace == "":
|
|
320
|
+
# In a GUI the user has a file open, and if the product wrote it, it names the workspace.
|
|
321
|
+
workspace = _Bridge.derive_workspace(getattr(bpy.data, "filepath", ""))
|
|
322
|
+
_BRIDGE = _Bridge(socket_path, {"workspace": workspace})
|
|
280
323
|
try:
|
|
281
324
|
_BRIDGE.start()
|
|
282
325
|
except Exception as exc:
|
|
@@ -300,6 +343,7 @@ def main():
|
|
|
300
343
|
"""Headless entry point: serve until the process is asked to stop."""
|
|
301
344
|
argv = sys.argv
|
|
302
345
|
options = {}
|
|
346
|
+
workspace = os.environ.get("DEEPBLEND_BRIDGE_WORKSPACE", "")
|
|
303
347
|
if "--" in argv:
|
|
304
348
|
tokens = argv[argv.index("--") + 1:]
|
|
305
349
|
index = 0
|
|
@@ -308,15 +352,23 @@ def main():
|
|
|
308
352
|
options["socket"] = tokens[index + 1]
|
|
309
353
|
index += 2
|
|
310
354
|
continue
|
|
355
|
+
if tokens[index] == "--workspace" and index + 1 < len(tokens):
|
|
356
|
+
workspace = tokens[index + 1]
|
|
357
|
+
index += 2
|
|
358
|
+
continue
|
|
311
359
|
index += 1
|
|
312
360
|
socket_path = options.get("socket", DEFAULT_SOCKET)
|
|
361
|
+
if workspace == "":
|
|
362
|
+
# WHAT THE OPEN FILE SAYS, when it says anything. An operator's explicit setting always wins;
|
|
363
|
+
# this only fills a silence, and only from a path that follows the product's own layout.
|
|
364
|
+
workspace = _Bridge.derive_workspace(getattr(bpy.data, "filepath", ""))
|
|
313
365
|
|
|
314
366
|
if bootstrap is None:
|
|
315
367
|
print(json.dumps({"kind": "error", "message": BOOTSTRAP_PROBLEM}), flush=True)
|
|
316
368
|
return 2
|
|
317
369
|
|
|
318
370
|
global _BRIDGE
|
|
319
|
-
_BRIDGE = _Bridge(socket_path, {"proc": None})
|
|
371
|
+
_BRIDGE = _Bridge(socket_path, {"proc": None, "workspace": workspace})
|
|
320
372
|
try:
|
|
321
373
|
_BRIDGE.start()
|
|
322
374
|
except Exception as exc:
|
|
@@ -326,7 +378,7 @@ def main():
|
|
|
326
378
|
# reports this in its panel; this is the same sentence for the headless one.
|
|
327
379
|
print(json.dumps({"kind": "error", "message": bootstrap.error_text(exc)}), flush=True)
|
|
328
380
|
return 2
|
|
329
|
-
print(json.dumps({"kind": "ready", "pid": os.getpid(), "socket": socket_path}), flush=True)
|
|
381
|
+
print(json.dumps({"kind": "ready", "pid": os.getpid(), "socket": socket_path, "workspace": workspace}), flush=True)
|
|
330
382
|
try:
|
|
331
383
|
# The serve thread is a daemon, so this loop is what keeps the process alive. A SIGTERM (or
|
|
332
384
|
# the product closing the socket and asking for shutdown) ends it.
|