@deepblend/dsh-blender-provider-local 0.2.9 → 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.9",
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.9"
14
+ "@deepblend/dsh-blender-contracts": "0.2.10"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {
@@ -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
@@ -187,7 +196,12 @@ class _Bridge:
187
196
  nothing — so the product's `BlenderSession.ready()` waited forever on a peer that was already
188
197
  serving. A transport that answers the same conversation has to open it the same way.
189
198
  """
190
- connection.sendall((json.dumps({"kind": "ready", "pid": os.getpid(), "socket": self.socket_path}) + "\n").encode("utf-8"))
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"))
191
205
  buffer = b""
192
206
  while True:
193
207
  chunk = connection.recv(65536)
@@ -267,6 +281,8 @@ class DEEPBLEND_PT_bridge(bpy.types.Panel):
267
281
  layout.label(text="Status: %s" % (_BRIDGE.status(),))
268
282
  layout.label(text="Socket: %s" % (os.path.basename(_BRIDGE.socket_path),))
269
283
  layout.label(text="Operations served: %d" % (_BRIDGE.requests,))
284
+ if _BRIDGE.workspace:
285
+ layout.label(text="Workspace: %s" % (os.path.basename(_BRIDGE.workspace),))
270
286
  if _BRIDGE.last_action is not None:
271
287
  layout.label(text="Last: %s" % (_BRIDGE.last_action,))
272
288
  if _BRIDGE.last_error is not None:
@@ -276,7 +292,7 @@ class DEEPBLEND_PT_bridge(bpy.types.Panel):
276
292
  def register():
277
293
  global _BRIDGE
278
294
  socket_path = os.environ.get("DEEPBLEND_BRIDGE_SOCKET", DEFAULT_SOCKET)
279
- _BRIDGE = _Bridge(socket_path)
295
+ _BRIDGE = _Bridge(socket_path, {"workspace": os.environ.get("DEEPBLEND_BRIDGE_WORKSPACE", "")})
280
296
  try:
281
297
  _BRIDGE.start()
282
298
  except Exception as exc:
@@ -300,6 +316,7 @@ def main():
300
316
  """Headless entry point: serve until the process is asked to stop."""
301
317
  argv = sys.argv
302
318
  options = {}
319
+ workspace = os.environ.get("DEEPBLEND_BRIDGE_WORKSPACE", "")
303
320
  if "--" in argv:
304
321
  tokens = argv[argv.index("--") + 1:]
305
322
  index = 0
@@ -308,6 +325,10 @@ def main():
308
325
  options["socket"] = tokens[index + 1]
309
326
  index += 2
310
327
  continue
328
+ if tokens[index] == "--workspace" and index + 1 < len(tokens):
329
+ workspace = tokens[index + 1]
330
+ index += 2
331
+ continue
311
332
  index += 1
312
333
  socket_path = options.get("socket", DEFAULT_SOCKET)
313
334
 
@@ -316,7 +337,7 @@ def main():
316
337
  return 2
317
338
 
318
339
  global _BRIDGE
319
- _BRIDGE = _Bridge(socket_path, {"proc": None})
340
+ _BRIDGE = _Bridge(socket_path, {"proc": None, "workspace": workspace})
320
341
  try:
321
342
  _BRIDGE.start()
322
343
  except Exception as exc:
@@ -326,7 +347,7 @@ def main():
326
347
  # reports this in its panel; this is the same sentence for the headless one.
327
348
  print(json.dumps({"kind": "error", "message": bootstrap.error_text(exc)}), flush=True)
328
349
  return 2
329
- print(json.dumps({"kind": "ready", "pid": os.getpid(), "socket": socket_path}), flush=True)
350
+ print(json.dumps({"kind": "ready", "pid": os.getpid(), "socket": socket_path, "workspace": workspace}), flush=True)
330
351
  try:
331
352
  # The serve thread is a daemon, so this loop is what keeps the process alive. A SIGTERM (or
332
353
  # the product closing the socket and asking for shutdown) ends it.