@deepblend/dsh-blender-provider-local 0.2.8 → 0.2.10
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.10",
|
|
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.10"
|
|
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
|
|
@@ -107,10 +116,23 @@ class _Bridge:
|
|
|
107
116
|
# -- lifecycle ---------------------------------------------------------
|
|
108
117
|
|
|
109
118
|
def start(self):
|
|
119
|
+
# THREE STATES, NOT TWO. A socket path that exists is either a LIVE bridge or the file a dead
|
|
120
|
+
# one left behind, and the difference decides everything.
|
|
121
|
+
#
|
|
122
|
+
# MEASURED, and this is why: two workspaces on one machine both enabled the add-on, and the
|
|
123
|
+
# SECOND bridge unlinked the first's socket file, bound its own, and announced `ready` — so the
|
|
124
|
+
# first Blender became unreachable while still believing it was listening, and the second
|
|
125
|
+
# reported success. That is the failure this repository keeps paying for: something other than
|
|
126
|
+
# what was asked, reported as fine.
|
|
110
127
|
if os.path.exists(self.socket_path):
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
if self._someone_is_listening():
|
|
129
|
+
raise RuntimeError(
|
|
130
|
+
"another Blender is already serving %s. Give this one its own path with "
|
|
131
|
+
"DEEPBLEND_BRIDGE_SOCKET (or the add-on's preferences), or stop the other bridge."
|
|
132
|
+
% (self.socket_path,)
|
|
133
|
+
)
|
|
134
|
+
# Nothing answers, so it is the file a process that died left behind. Refusing here would
|
|
135
|
+
# leave the user with no way forward except deleting a file they cannot see.
|
|
114
136
|
os.unlink(self.socket_path)
|
|
115
137
|
self.server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
116
138
|
self.server.bind(self.socket_path)
|
|
@@ -118,6 +140,21 @@ class _Bridge:
|
|
|
118
140
|
self.thread = threading.Thread(target=self._serve, name="deepblend-bridge", daemon=True)
|
|
119
141
|
self.thread.start()
|
|
120
142
|
|
|
143
|
+
def _someone_is_listening(self):
|
|
144
|
+
"""Whether a live bridge is behind this path, rather than a file left by a dead one."""
|
|
145
|
+
probe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
146
|
+
probe.settimeout(1.0)
|
|
147
|
+
try:
|
|
148
|
+
probe.connect(self.socket_path)
|
|
149
|
+
return True
|
|
150
|
+
except Exception:
|
|
151
|
+
return False
|
|
152
|
+
finally:
|
|
153
|
+
try:
|
|
154
|
+
probe.close()
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
121
158
|
def stop(self):
|
|
122
159
|
try:
|
|
123
160
|
if self.server is not None:
|
|
@@ -159,7 +196,12 @@ class _Bridge:
|
|
|
159
196
|
nothing — so the product's `BlenderSession.ready()` waited forever on a peer that was already
|
|
160
197
|
serving. A transport that answers the same conversation has to open it the same way.
|
|
161
198
|
"""
|
|
162
|
-
connection.sendall((json.dumps({
|
|
199
|
+
connection.sendall((json.dumps({
|
|
200
|
+
"kind": "ready",
|
|
201
|
+
"pid": os.getpid(),
|
|
202
|
+
"socket": self.socket_path,
|
|
203
|
+
"workspace": self.workspace,
|
|
204
|
+
}) + "\n").encode("utf-8"))
|
|
163
205
|
buffer = b""
|
|
164
206
|
while True:
|
|
165
207
|
chunk = connection.recv(65536)
|
|
@@ -239,6 +281,8 @@ class DEEPBLEND_PT_bridge(bpy.types.Panel):
|
|
|
239
281
|
layout.label(text="Status: %s" % (_BRIDGE.status(),))
|
|
240
282
|
layout.label(text="Socket: %s" % (os.path.basename(_BRIDGE.socket_path),))
|
|
241
283
|
layout.label(text="Operations served: %d" % (_BRIDGE.requests,))
|
|
284
|
+
if _BRIDGE.workspace:
|
|
285
|
+
layout.label(text="Workspace: %s" % (os.path.basename(_BRIDGE.workspace),))
|
|
242
286
|
if _BRIDGE.last_action is not None:
|
|
243
287
|
layout.label(text="Last: %s" % (_BRIDGE.last_action,))
|
|
244
288
|
if _BRIDGE.last_error is not None:
|
|
@@ -248,7 +292,7 @@ class DEEPBLEND_PT_bridge(bpy.types.Panel):
|
|
|
248
292
|
def register():
|
|
249
293
|
global _BRIDGE
|
|
250
294
|
socket_path = os.environ.get("DEEPBLEND_BRIDGE_SOCKET", DEFAULT_SOCKET)
|
|
251
|
-
_BRIDGE = _Bridge(socket_path)
|
|
295
|
+
_BRIDGE = _Bridge(socket_path, {"workspace": os.environ.get("DEEPBLEND_BRIDGE_WORKSPACE", "")})
|
|
252
296
|
try:
|
|
253
297
|
_BRIDGE.start()
|
|
254
298
|
except Exception as exc:
|
|
@@ -272,6 +316,7 @@ def main():
|
|
|
272
316
|
"""Headless entry point: serve until the process is asked to stop."""
|
|
273
317
|
argv = sys.argv
|
|
274
318
|
options = {}
|
|
319
|
+
workspace = os.environ.get("DEEPBLEND_BRIDGE_WORKSPACE", "")
|
|
275
320
|
if "--" in argv:
|
|
276
321
|
tokens = argv[argv.index("--") + 1:]
|
|
277
322
|
index = 0
|
|
@@ -280,6 +325,10 @@ def main():
|
|
|
280
325
|
options["socket"] = tokens[index + 1]
|
|
281
326
|
index += 2
|
|
282
327
|
continue
|
|
328
|
+
if tokens[index] == "--workspace" and index + 1 < len(tokens):
|
|
329
|
+
workspace = tokens[index + 1]
|
|
330
|
+
index += 2
|
|
331
|
+
continue
|
|
283
332
|
index += 1
|
|
284
333
|
socket_path = options.get("socket", DEFAULT_SOCKET)
|
|
285
334
|
|
|
@@ -288,9 +337,17 @@ def main():
|
|
|
288
337
|
return 2
|
|
289
338
|
|
|
290
339
|
global _BRIDGE
|
|
291
|
-
_BRIDGE = _Bridge(socket_path, {"proc": None})
|
|
292
|
-
|
|
293
|
-
|
|
340
|
+
_BRIDGE = _Bridge(socket_path, {"proc": None, "workspace": workspace})
|
|
341
|
+
try:
|
|
342
|
+
_BRIDGE.start()
|
|
343
|
+
except Exception as exc:
|
|
344
|
+
# A REFUSAL IS A RESULT, NOT A TRACEBACK. The one thing that can go wrong here is a named,
|
|
345
|
+
# actionable condition — another bridge on the same path — and a caller parsing stdout should
|
|
346
|
+
# get a document it can read rather than a stack it has to scrape. The add-on path already
|
|
347
|
+
# reports this in its panel; this is the same sentence for the headless one.
|
|
348
|
+
print(json.dumps({"kind": "error", "message": bootstrap.error_text(exc)}), flush=True)
|
|
349
|
+
return 2
|
|
350
|
+
print(json.dumps({"kind": "ready", "pid": os.getpid(), "socket": socket_path, "workspace": workspace}), flush=True)
|
|
294
351
|
try:
|
|
295
352
|
# The serve thread is a daemon, so this loop is what keeps the process alive. A SIGTERM (or
|
|
296
353
|
# the product closing the socket and asking for shutdown) ends it.
|