@deepblend/dsh-blender-provider-local 0.2.11 → 0.2.13

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
@@ -591,6 +591,14 @@ export default class LocalBlenderRuntime extends Service {
591
591
  /** The kept Blender for `sessionActions`, and the timer that closes it when it goes quiet. */
592
592
  _session = null
593
593
  _sessionTimer = null
594
+ /**
595
+ * Which transport the kept session came from: `configured`, `discovered` or `spawned`.
596
+ *
597
+ * Recorded rather than inferred, because "is this deployment driving the user's Blender or one of
598
+ * its own" is the first question anybody asks of a Live Bridge that behaves unexpectedly — and the
599
+ * only place that knows is the branch that chose.
600
+ */
601
+ _sessionOrigin = null
594
602
 
595
603
  /**
596
604
  * @param {import('@deepseek-ai/cordis').Context} ctx
@@ -1021,12 +1029,39 @@ export default class LocalBlenderRuntime extends Service {
1021
1029
  */
1022
1030
  async _keptSession() {
1023
1031
  if (this._session !== null && !this._session.closed) return this._session
1024
- const socket = this.config.sessionSocket
1025
- // THE USER'S BLENDER, when they have named one. `attachSession` owns the refusal and its message;
1026
- // this only decides which of the two transports the configured actions travel over.
1027
- this._session = typeof socket === 'string' && socket.length > 0
1028
- ? await this.attachSession(socket)
1029
- : await this.openSession()
1032
+
1033
+ const configured = this.config.sessionSocket
1034
+ if (typeof configured === 'string' && configured.length > 0) {
1035
+ // NAMED BY THE OPERATOR, so it is used and a failure is theirs to see. `attachSession` owns the
1036
+ // refusal and its message; this only decides which transport the configured actions travel over.
1037
+ this._session = await this.attachSession(configured)
1038
+ this._sessionOrigin = 'configured'
1039
+ return this._session
1040
+ }
1041
+
1042
+ // DISCOVERY, WHICH IS NOT A GUESS. The convention says a bridge listens at
1043
+ // `<workspace>/.deepblend/bridge.sock`, and a deployment that opted into sessions should not have
1044
+ // to write that path down as well. A GUESS asserts and fails when it is wrong; this CHECKS and
1045
+ // falls back — something answering at the conventional path is used, and nothing answering means
1046
+ // exactly what happened before this existed. The wrong-blender check still applies to whatever is
1047
+ // found, so discovery cannot attach to another workspace's Blender.
1048
+ const conventional = join(this.workspaceRoot, '.deepblend', 'bridge.sock')
1049
+ if (existsSync(conventional)) {
1050
+ const found = await this.attachSession(conventional).then(
1051
+ session => session,
1052
+ () => null,
1053
+ )
1054
+ if (found !== null) {
1055
+ this._session = found
1056
+ this._sessionOrigin = 'discovered'
1057
+ return this._session
1058
+ }
1059
+ // A file that exists and does not answer is the state a crash leaves; the bridge replaces it
1060
+ // when it starts again, and until then this deployment spawns its own Blender as before.
1061
+ }
1062
+
1063
+ this._session = await this.openSession()
1064
+ this._sessionOrigin = 'spawned'
1030
1065
  return this._session
1031
1066
  }
1032
1067
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-provider-local",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
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.11"
14
+ "@deepblend/dsh-blender-contracts": "0.2.13"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {
@@ -90,6 +90,26 @@ bl_info = {
90
90
  # add-on uses when a user enables it with no configuration.
91
91
  DEFAULT_SOCKET = os.path.join(os.path.expanduser("~"), ".deepblend-bridge.sock")
92
92
 
93
+ # THE PER-WORKSPACE CONVENTION, and the reason it exists: the global default above is ONE path for the
94
+ # whole machine, so two workspaces with a Blender each collide there — the second refuses now, which is
95
+ # honest but is still the user's problem to solve by hand. When a bridge knows its workspace, it can
96
+ # simply listen somewhere that belongs to that workspace, and the collision never happens.
97
+ #
98
+ # The path is inside `.deepblend/`, which is the store and is gitignored — a socket is a runtime object,
99
+ # not a source file, and it must never be committable.
100
+ WORKSPACE_SOCKET_RELATIVE = os.path.join(".deepblend", "bridge.sock")
101
+
102
+
103
+ def default_socket_for(workspace):
104
+ """Where a bridge should listen, given what it knows.
105
+
106
+ A workspace it knows gives a path that belongs to that workspace; a workspace it does not know
107
+ gives the machine-wide default, which is what every deployment had before this convention.
108
+ """
109
+ if isinstance(workspace, str) and workspace != "":
110
+ return os.path.join(workspace, WORKSPACE_SOCKET_RELATIVE)
111
+ return DEFAULT_SOCKET
112
+
93
113
 
94
114
  class _Bridge:
95
115
  """The serving state: one socket, one thread, and the last thing that happened."""
@@ -134,6 +154,11 @@ class _Bridge:
134
154
  # Nothing answers, so it is the file a process that died left behind. Refusing here would
135
155
  # leave the user with no way forward except deleting a file they cannot see.
136
156
  os.unlink(self.socket_path)
157
+ # A workspace-derived path lives inside `.deepblend/`, which may not exist yet on a machine
158
+ # where the product has never run — and `bind` does not create directories.
159
+ parent = os.path.dirname(self.socket_path)
160
+ if parent and not os.path.isdir(parent):
161
+ os.makedirs(parent, exist_ok=True)
137
162
  self.server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
138
163
  self.server.bind(self.socket_path)
139
164
  self.server.listen(4)
@@ -314,11 +339,11 @@ class DEEPBLEND_PT_bridge(bpy.types.Panel):
314
339
 
315
340
  def register():
316
341
  global _BRIDGE
317
- socket_path = os.environ.get("DEEPBLEND_BRIDGE_SOCKET", DEFAULT_SOCKET)
318
342
  workspace = os.environ.get("DEEPBLEND_BRIDGE_WORKSPACE", "")
319
343
  if workspace == "":
320
344
  # In a GUI the user has a file open, and if the product wrote it, it names the workspace.
321
345
  workspace = _Bridge.derive_workspace(getattr(bpy.data, "filepath", ""))
346
+ socket_path = os.environ.get("DEEPBLEND_BRIDGE_SOCKET") or default_socket_for(workspace)
322
347
  _BRIDGE = _Bridge(socket_path, {"workspace": workspace})
323
348
  try:
324
349
  _BRIDGE.start()
@@ -357,11 +382,13 @@ def main():
357
382
  index += 2
358
383
  continue
359
384
  index += 1
360
- socket_path = options.get("socket", DEFAULT_SOCKET)
361
385
  if workspace == "":
362
386
  # WHAT THE OPEN FILE SAYS, when it says anything. An operator's explicit setting always wins;
363
387
  # this only fills a silence, and only from a path that follows the product's own layout.
364
388
  workspace = _Bridge.derive_workspace(getattr(bpy.data, "filepath", ""))
389
+ # THE SOCKET FOLLOWS THE WORKSPACE, so two workspaces with a Blender each do not collide at all.
390
+ # An explicit DEEPBLEND_BRIDGE_SOCKET still wins, exactly as an explicit workspace does.
391
+ socket_path = options.get("socket") or os.environ.get("DEEPBLEND_BRIDGE_SOCKET") or default_socket_for(workspace)
365
392
 
366
393
  if bootstrap is None:
367
394
  print(json.dumps({"kind": "error", "message": BOOTSTRAP_PROBLEM}), flush=True)