0.11.0 Multi LW

This commit is contained in:
2026-03-17 19:58:32 +00:00
parent 16443c8a45
commit 16c783e1aa
25 changed files with 811 additions and 960 deletions
+154 -9
View File
@@ -190,6 +190,7 @@ class DiskDetectionService extends EventEmitter {
this.timer = null;
this.lastDetected = null;
this.lastPresent = false;
this.detectedDiscs = new Map(); // devicePath → device object (multi-drive tracking)
this.deviceLocks = new Map();
this.pollingSuspended = false;
}
@@ -248,8 +249,8 @@ class DiskDetectionService extends EventEmitter {
if (this.pollingSuspended) {
logger.debug('poll:skip:suspended', { nextDelay });
} else if (autoDetectionEnabled) {
const detected = await this.detectDisc(map);
this.applyDetectionResult(detected, { forceInsertEvent: false });
const detectedList = await this.detectAllDiscs(map);
this.applyMultiDetectionResults(detectedList, { forceInsertEvent: false });
} else {
logger.debug('poll:skip:auto-detection-disabled', { nextDelay });
}
@@ -270,17 +271,30 @@ class DiskDetectionService extends EventEmitter {
driveDevice: map.drive_device
});
const detected = await this.detectDisc(map);
const result = this.applyDetectionResult(detected, { forceInsertEvent: true });
const detectedList = await this.detectAllDiscs(map);
const results = this.applyMultiDetectionResults(detectedList, { forceInsertEvent: true });
const insertedResults = results.filter((r) => r.emitted === 'discInserted');
const removedResults = results.filter((r) => r.emitted === 'discRemoved');
const present = detectedList.length > 0;
const firstInserted = insertedResults[0] || null;
logger.info('rescan:done', {
present: result.present,
emitted: result.emitted,
changed: result.changed,
detected: result.device || null
driveCount: detectedList.length,
inserted: insertedResults.length,
removed: removedResults.length
});
return result;
// Backward-compat return: report on first inserted device (or removed if nothing inserted)
return {
present,
changed: insertedResults.length > 0 || removedResults.length > 0,
emitted: insertedResults.length > 0
? 'discInserted'
: (removedResults.length > 0 ? 'discRemoved' : 'none'),
device: firstInserted?.device || null,
allDetected: detectedList
};
} catch (error) {
logger.error('rescan:error', { error: errorToMeta(error) });
throw error;
@@ -425,6 +439,83 @@ class DiskDetectionService extends EventEmitter {
return this.detectAuto();
}
// Returns ALL discs with media (not just the first one)
async detectAllDiscs(settingsMap) {
if (settingsMap.drive_mode === 'explicit') {
// drive_devices is a JSON array of paths; fall back to legacy drive_device
let explicitPaths = [];
try {
const parsed = JSON.parse(settingsMap.drive_devices || '[]');
if (Array.isArray(parsed)) {
explicitPaths = parsed.map((p) => String(p || '').trim()).filter(Boolean);
}
} catch (_error) {
// malformed JSON — ignore, fall through to legacy
}
if (explicitPaths.length === 0) {
const legacy = String(settingsMap.drive_device || '').trim();
if (legacy) {
explicitPaths = [legacy];
}
}
const results = await Promise.all(explicitPaths.map((p) => this.detectExplicit(p)));
return results.filter(Boolean);
}
return this.detectAllAuto();
}
// Multi-drive: tracks per-device state and emits insert/remove events for each
applyMultiDetectionResults(detectedList, { forceInsertEvent = false } = {}) {
const detected = Array.isArray(detectedList) ? detectedList : [];
const newMap = new Map();
for (const device of detected) {
if (device?.path) {
newMap.set(device.path, device);
}
}
const results = [];
// Check for new or changed devices
for (const [devicePath, device] of newMap) {
const previous = this.detectedDiscs.get(devicePath);
const changed = previous ? buildSignature(previous) !== buildSignature(device) : false;
const shouldEmitInserted = forceInsertEvent || !previous || changed;
if (shouldEmitInserted) {
logger.info('disc:inserted', { device, forceInsertEvent, changed });
this.emit('discInserted', device);
results.push({ path: devicePath, emitted: 'discInserted', device });
} else {
results.push({ path: devicePath, emitted: 'none', device });
}
}
// Check for removed devices
for (const [devicePath, device] of this.detectedDiscs) {
if (!newMap.has(devicePath)) {
logger.info('disc:removed', { device });
this.emit('discRemoved', device);
results.push({ path: devicePath, emitted: 'discRemoved', device: null });
}
}
this.detectedDiscs = newMap;
// Update legacy single-disc tracking for backward compat
if (newMap.size > 0) {
this.lastDetected = Array.from(newMap.values())[0];
this.lastPresent = true;
} else {
if (this.lastPresent) {
this.lastDetected = null;
this.lastPresent = false;
}
}
return results;
}
async detectExplicit(devicePath) {
if (this.isDeviceLocked(devicePath)) {
logger.debug('detect:explicit:locked', {
@@ -527,6 +618,60 @@ class DiskDetectionService extends EventEmitter {
return null;
}
async detectAllAuto() {
const details = await this.getBlockDeviceInfo();
const romCandidates = details.filter((entry) => entry.type === 'rom');
const results = [];
for (const item of romCandidates) {
const path = item.path || (item.name ? `/dev/${item.name}` : null);
if (!path) {
continue;
}
if (this.isDeviceLocked(path)) {
logger.debug('detect:all-auto:skip-locked', {
path,
activeLocks: this.getActiveLocks()
});
continue;
}
const mediaState = await this.checkMediaPresent(path);
if (!mediaState.hasMedia) {
continue;
}
const discLabel = await this.getDiscLabel(path);
const detectedFsType = String(item.fstype || mediaState.type || '').trim() || null;
const mediaProfile = await this.inferMediaProfile(path, {
discLabel,
label: item.label,
model: item.model,
fstype: detectedFsType,
mountpoint: item.mountpoint
});
const detected = {
mode: 'auto',
path,
name: item.name,
model: item.model || 'Optical Drive',
label: item.label || null,
discLabel: discLabel || null,
mountpoint: item.mountpoint || null,
fstype: detectedFsType,
mediaProfile: mediaProfile || null,
index: this.guessDiscIndex(item.name)
};
logger.debug('detect:all-auto:found', { detected });
results.push(detected);
}
logger.debug('detect:all-auto:done', { count: results.length });
return results;
}
async getBlockDeviceInfo() {
try {
const { stdout } = await execFileAsync('lsblk', [