@iobroker/js-controller-cli 8.0.0-alpha.1-20260921-e5941ca8a → 8.0.0-alpha.3-20260924-a3df0c4fe

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.
@@ -204,6 +204,21 @@ export declare class Install {
204
204
  * @param adapter adapter name
205
205
  */
206
206
  deleteAdapter(adapter: string): Promise<EXIT_CODES>;
207
+ /**
208
+ * Remove the virtual environment a Python adapter leaves behind in the data directory
209
+ *
210
+ * Host-local state like `node_modules` - rebuildable, but easily a few hundred megabytes - so it goes when
211
+ * the adapter goes. Deliberately not part of the npm uninstall: that one starts by resolving the adapter's
212
+ * `io-package.json`, so for a half-removed installation, where the package directory is already gone, it
213
+ * throws before ever getting here - and that is precisely the case which leaves the environment behind with
214
+ * nothing pointing at it.
215
+ *
216
+ * Removed without looking at `common.platform` for the same reason: the directory only exists for Python
217
+ * adapters, and the `io-package.json` that would say so may no longer be there.
218
+ *
219
+ * @param adapter adapter name, e.g. "pyexample"
220
+ */
221
+ private _removePythonEnvironment;
207
222
  /**
208
223
  * Deletes given instance of an adapter
209
224
  *
@@ -211,6 +226,21 @@ export declare class Install {
211
226
  * @param instance e.g. 1, if undefined deletes all instances
212
227
  */
213
228
  deleteInstance(adapter: string, instance?: number): Promise<void | EXIT_CODES.CANNOT_DELETE_DEPENDENCY>;
229
+ /**
230
+ * Free all exclusive resources (serial ports, TCP/UDP ports, ...) the given instance(s) had registered.
231
+ *
232
+ * This is needed because an instance can be deleted via the CLI while a js-controller is not running: it
233
+ * cannot notice that the instance object disappeared, so its `system.host.<host>.usedResources.<type>`
234
+ * states would keep listing the resources of an instance that no longer exists.
235
+ *
236
+ * Hosts that are running are deliberately skipped. Such a host cleans its own registry up when it sees the
237
+ * instance object being deleted, and it holds the registry in memory: it rewrites the state from that copy
238
+ * on its next change, which would silently undo a write made here.
239
+ *
240
+ * @param adapter adapter name, e.g. "mqtt"
241
+ * @param instance instance number; if undefined, all instances of the adapter are freed
242
+ */
243
+ private _freeUsedResources;
214
244
  /**
215
245
  * Remove all node modules that has been installed by this instance
216
246
  *
@@ -1126,6 +1126,7 @@ class Install {
1126
1126
  }
1127
1127
  await this._removeCustomFromObjects([adapter]);
1128
1128
  await _uninstallNpm();
1129
+ await this._removePythonEnvironment(adapter);
1129
1130
  } else {
1130
1131
  const dependentInstance = await this._hasDependentInstances(adapter);
1131
1132
  if (dependentInstance) {
@@ -1143,16 +1144,43 @@ class Install {
1143
1144
  await this._deleteAdapterFiles(adapter, metaFilesToDelete);
1144
1145
  await this._deleteAdapterObjects(knownObjectIDs);
1145
1146
  await this._deleteAdapterStates(knownStateIDs);
1147
+ await this._freeUsedResources(adapter);
1146
1148
  if (this.params.custom) {
1147
1149
  await this._removeCustomFromObjects([...instances, adapter]);
1148
1150
  }
1149
1151
  await _uninstallNpm();
1152
+ await this._removePythonEnvironment(adapter);
1150
1153
  }
1151
1154
  } catch (e) {
1152
1155
  console.error(`There was an error uninstalling ${adapter} on ${hostname}: ${e.message}`);
1153
1156
  }
1154
1157
  return resultCode;
1155
1158
  }
1159
+ /**
1160
+ * Remove the virtual environment a Python adapter leaves behind in the data directory
1161
+ *
1162
+ * Host-local state like `node_modules` - rebuildable, but easily a few hundred megabytes - so it goes when
1163
+ * the adapter goes. Deliberately not part of the npm uninstall: that one starts by resolving the adapter's
1164
+ * `io-package.json`, so for a half-removed installation, where the package directory is already gone, it
1165
+ * throws before ever getting here - and that is precisely the case which leaves the environment behind with
1166
+ * nothing pointing at it.
1167
+ *
1168
+ * Removed without looking at `common.platform` for the same reason: the directory only exists for Python
1169
+ * adapters, and the `io-package.json` that would say so may no longer be there.
1170
+ *
1171
+ * @param adapter adapter name, e.g. "pyexample"
1172
+ */
1173
+ async _removePythonEnvironment(adapter) {
1174
+ try {
1175
+ const pythonEnvDir = import_js_controller_common.tools.getPythonEnvDir(adapter);
1176
+ if (await import_fs_extra.default.pathExists(pythonEnvDir)) {
1177
+ await import_fs_extra.default.remove(pythonEnvDir);
1178
+ console.log(`host.${hostname} Python environment "${pythonEnvDir}" deleted`);
1179
+ }
1180
+ } catch (e) {
1181
+ console.error(`Cannot delete the Python environment of ${adapter}: ${e.message}`);
1182
+ }
1183
+ }
1156
1184
  /**
1157
1185
  * Deletes given instance of an adapter
1158
1186
  *
@@ -1176,10 +1204,79 @@ class Install {
1176
1204
  await this._enumerateAdapterDocs(knownObjectIDs, adapter, instance);
1177
1205
  await this._deleteAdapterObjects(knownObjectIDs);
1178
1206
  await this._deleteAdapterStates(knownStateIDs);
1207
+ await this._freeUsedResources(adapter, instance);
1179
1208
  if (this.params.custom) {
1180
1209
  await this._removeCustomFromObjects([`${adapter}.${instance}`]);
1181
1210
  }
1182
1211
  }
1212
+ /**
1213
+ * Free all exclusive resources (serial ports, TCP/UDP ports, ...) the given instance(s) had registered.
1214
+ *
1215
+ * This is needed because an instance can be deleted via the CLI while a js-controller is not running: it
1216
+ * cannot notice that the instance object disappeared, so its `system.host.<host>.usedResources.<type>`
1217
+ * states would keep listing the resources of an instance that no longer exists.
1218
+ *
1219
+ * Hosts that are running are deliberately skipped. Such a host cleans its own registry up when it sees the
1220
+ * instance object being deleted, and it holds the registry in memory: it rewrites the state from that copy
1221
+ * on its next change, which would silently undo a write made here.
1222
+ *
1223
+ * @param adapter adapter name, e.g. "mqtt"
1224
+ * @param instance instance number; if undefined, all instances of the adapter are freed
1225
+ */
1226
+ async _freeUsedResources(adapter, instance) {
1227
+ const matches = /* @__PURE__ */ __name((namespace) => instance !== void 0 ? namespace === `${adapter}.${instance}` : namespace.startsWith(`${adapter}.`), "matches");
1228
+ let keys;
1229
+ try {
1230
+ keys = await this.states.getKeys("system.host.*.usedResources.*");
1231
+ } catch (e) {
1232
+ console.warn(`Cannot read the used resources registry: ${e.message}`);
1233
+ return;
1234
+ }
1235
+ if (!keys?.length) {
1236
+ return;
1237
+ }
1238
+ const hostPrefix = "system.host.";
1239
+ const registryMarker = ".usedResources.";
1240
+ const hostIsRunning = /* @__PURE__ */ new Map();
1241
+ for (const id of keys) {
1242
+ const markerIndex = id.lastIndexOf(registryMarker);
1243
+ if (!id.startsWith(hostPrefix) || markerIndex <= hostPrefix.length) {
1244
+ continue;
1245
+ }
1246
+ const host = id.substring(hostPrefix.length, markerIndex);
1247
+ let isRunning = hostIsRunning.get(host);
1248
+ if (isRunning === void 0) {
1249
+ try {
1250
+ const aliveState = await this.states.getState(`${hostPrefix}${host}.alive`);
1251
+ isRunning = aliveState?.val === true;
1252
+ } catch (e) {
1253
+ console.warn(`Cannot check whether host "${host}" is running: ${e.message}`);
1254
+ isRunning = true;
1255
+ }
1256
+ hostIsRunning.set(host, isRunning);
1257
+ }
1258
+ if (isRunning) {
1259
+ continue;
1260
+ }
1261
+ try {
1262
+ const state = await this.states.getState(id);
1263
+ if (!state || typeof state.val !== "string" || !state.val) {
1264
+ continue;
1265
+ }
1266
+ const parsed = JSON.parse(state.val);
1267
+ if (!Array.isArray(parsed)) {
1268
+ continue;
1269
+ }
1270
+ const original = parsed;
1271
+ const filtered = original.filter((entry) => typeof entry?.instance !== "string" || !matches(entry.instance));
1272
+ if (filtered.length !== original.length) {
1273
+ await this.states.setStateAsync(id, { val: JSON.stringify(filtered), ack: true });
1274
+ }
1275
+ } catch (e) {
1276
+ console.warn(`Cannot free the used resources in "${id}": ${e.message}`);
1277
+ }
1278
+ }
1279
+ }
1183
1280
  /**
1184
1281
  * Remove all node modules that has been installed by this instance
1185
1282
  *