feat(native): harden launcher bridge and implement presentation-aware handover
- Upgraded LauncherBackgroundSync to force-hydrate OS metadata (home/tmp) on mount. - Hardened electron_relay.ts with robust placeholder resolution and global regex. - Restored safe handover by making native.launch_from_cache presentation-aware. - Integrated heartbeat and sync status into the formal Launcher Config UI. - Added comprehensive technical documentation for the 2026 native architecture.
This commit is contained in:
@@ -1333,7 +1333,7 @@ exports.run_osascript = async function ({
|
||||
};
|
||||
|
||||
// Run raw command
|
||||
// Updated 2022-05-07
|
||||
// Updated 2026-01-26
|
||||
exports.run_cmd = async function ({
|
||||
cmd = null,
|
||||
return_stdout = null,
|
||||
@@ -1342,12 +1342,18 @@ exports.run_cmd = async function ({
|
||||
}) {
|
||||
console.log('*** Electron framework export: run_cmd() ***');
|
||||
|
||||
console.log(`Command String: ${cmd}`);
|
||||
// Resolve placeholders in the command string
|
||||
let cleaned_cmd = cmd;
|
||||
if (cmd && typeof cmd === 'string') {
|
||||
cleaned_cmd = cmd.replace(/\[home\]/g, home_directory).replace(/\[tmp\]/g, tmp_directory);
|
||||
}
|
||||
|
||||
console.log(`Command String: ${cleaned_cmd}`);
|
||||
|
||||
let result;
|
||||
|
||||
if (!sync) {
|
||||
result = child_process.exec(cmd, (err, stdout, stdin) => {
|
||||
result = child_process.exec(cleaned_cmd, (err, stdout, stdin) => {
|
||||
// if (err) throw err;
|
||||
if (err) {
|
||||
console.log('Error:', err);
|
||||
@@ -1366,7 +1372,7 @@ exports.run_cmd = async function ({
|
||||
}
|
||||
});
|
||||
} else {
|
||||
result = child_process.execSync(cmd, (err, stdout, stdin) => {
|
||||
result = child_process.execSync(cleaned_cmd, (err, stdout, stdin) => {
|
||||
// if (err) throw err;
|
||||
if (err) {
|
||||
console.log('Error:', err);
|
||||
@@ -1391,16 +1397,22 @@ exports.run_cmd = async function ({
|
||||
};
|
||||
|
||||
// Run raw command sync
|
||||
// Updated 2022-05-07
|
||||
// Updated 2026-01-26
|
||||
exports.run_cmd_sync = function ({ cmd = null, return_stdout = null, return_stdin = null }) {
|
||||
console.log('*** Electron framework export: run_cmd() ***');
|
||||
console.log('*** Electron framework export: run_cmd_sync() ***');
|
||||
|
||||
console.log(`Command String: ${cmd}`);
|
||||
// Resolve placeholders in the command string
|
||||
let cleaned_cmd = cmd;
|
||||
if (cmd && typeof cmd === 'string') {
|
||||
cleaned_cmd = cmd.replace(/\[home\]/g, home_directory).replace(/\[tmp\]/g, tmp_directory);
|
||||
}
|
||||
|
||||
console.log(`Command String: ${cleaned_cmd}`);
|
||||
|
||||
let stdout;
|
||||
|
||||
try {
|
||||
stdout = child_process.execSync(cmd, { encoding: 'utf8' });
|
||||
stdout = child_process.execSync(cleaned_cmd, { encoding: 'utf8' });
|
||||
console.log('Std Out:', stdout);
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
@@ -1414,40 +1426,10 @@ exports.run_cmd_sync = function ({ cmd = null, return_stdout = null, return_stdi
|
||||
console.log('Finished and returning true');
|
||||
return true;
|
||||
}
|
||||
|
||||
// let result;
|
||||
// let stdout;
|
||||
// let stderr;
|
||||
// try {
|
||||
// let { stdout, stderr } = child_process.execSync(cmd, (err, stdout, stdin) => {
|
||||
// // if (err) throw err;
|
||||
// if (err) {
|
||||
// console.log('Error:', err);
|
||||
// return false;
|
||||
// };
|
||||
|
||||
// console.log('stdout:', stdout);
|
||||
// // console.log('stdin:', stdin);
|
||||
|
||||
// if (return_stdout) {
|
||||
// console.log('Finished and returning stdout');
|
||||
// return stdout;
|
||||
// } else {
|
||||
// console.log('Finished and returning true');
|
||||
// return true;
|
||||
// }
|
||||
// });
|
||||
// } catch (err) {
|
||||
// console.error(err);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// console.log('Result:', result);
|
||||
// return result;
|
||||
};
|
||||
|
||||
// Run raw command
|
||||
// Updated 2022-05-25
|
||||
// Updated 2026-01-26
|
||||
exports.get_device_info = async function () {
|
||||
console.log('*** Electron framework export: get_device_info() ***');
|
||||
|
||||
@@ -1465,11 +1447,134 @@ exports.get_device_info = async function () {
|
||||
data['release'] = os.release();
|
||||
data['uptime'] = os.uptime();
|
||||
data['version'] = os.version();
|
||||
|
||||
// Add directory info for placeholder resolution in UI
|
||||
data['home_directory'] = home_directory;
|
||||
data['tmp_directory'] = tmp_directory;
|
||||
|
||||
console.log(data);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Atomic Copy-and-Launch (Phase 2/5)
|
||||
* Moves a file from the hashed cache to the operational temp directory
|
||||
* and triggers the system launcher.
|
||||
*/
|
||||
exports.launch_from_cache = async function ({
|
||||
cache_root,
|
||||
hash,
|
||||
temp_root,
|
||||
filename,
|
||||
hash_prefix_length = 2
|
||||
}) {
|
||||
console.log('*** Aether App Native export: launch_from_cache() ***');
|
||||
|
||||
// 1. Resolve Path Placeholders (using global regex)
|
||||
const clean_cache_root = cache_root.replace(/\[home\]/g, home_directory).replace(/\[tmp\]/g, tmp_directory);
|
||||
const clean_temp_root = temp_root.replace(/\[home\]/g, home_directory).replace(/\[tmp\]/g, tmp_directory);
|
||||
|
||||
const hash_filename = `${hash}.file`;
|
||||
const prefix = hash.substring(0, hash_prefix_length);
|
||||
const source_path = path.join(clean_cache_root, prefix, hash_filename);
|
||||
const dest_path = path.join(clean_temp_root, filename);
|
||||
|
||||
console.log(`Source: ${source_path}`);
|
||||
console.log(`Dest: ${dest_path}`);
|
||||
|
||||
try {
|
||||
// 2. Ensure temp directory exists
|
||||
if (!fs.existsSync(clean_temp_root)) {
|
||||
fs.mkdirSync(clean_temp_root, { recursive: true });
|
||||
}
|
||||
|
||||
// 3. Verify Source
|
||||
if (!fs.existsSync(source_path)) {
|
||||
throw new Error(`Source file not found in cache: ${source_path}`);
|
||||
}
|
||||
|
||||
// 4. Perform atomic copy
|
||||
fs.copyFileSync(source_path, dest_path);
|
||||
console.log('File copied to temp successfully.');
|
||||
|
||||
// 5. Trigger Specialized Launcher (if presentation)
|
||||
const ext = path.extname(filename).toLowerCase().replace('.', '');
|
||||
const is_pres = ['pptx', 'ppt', 'key', 'pdf', 'odp'].includes(ext);
|
||||
|
||||
if (is_pres) {
|
||||
return await exports.launch_presentation({
|
||||
path: dest_path,
|
||||
app: ext === 'key' ? 'keynote' : 'default'
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Default Fallback
|
||||
return await ipcRenderer.invoke('open_local_file', '', dest_path);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Launch Error:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Specialized Presentation Launcher (Phase 5)
|
||||
* Handles platform-specific application selection (LibreOffice on Linux,
|
||||
* PowerPoint/Keynote on macOS).
|
||||
* Updated 2026-01-26
|
||||
*/
|
||||
exports.launch_presentation = async function ({
|
||||
path: raw_path,
|
||||
app = 'default',
|
||||
os_platform = 'auto'
|
||||
}) {
|
||||
console.log('*** Aether App Native export: launch_presentation() ***');
|
||||
|
||||
// Resolve placeholders if they exist in the incoming path (using global regex)
|
||||
let cleaned_path = raw_path
|
||||
.replace(/\[home\]/g, home_directory)
|
||||
.replace(/\[tmp\]/g, tmp_directory);
|
||||
|
||||
console.log(`Raw Path: ${raw_path}; Cleaned Path: ${cleaned_path}; App: ${app}; OS: ${os_platform}`);
|
||||
|
||||
// 1. Detect OS
|
||||
let platform = os_platform;
|
||||
if (platform === 'auto') {
|
||||
platform = os.platform();
|
||||
}
|
||||
|
||||
// 2. Handle Linux (LibreOffice Testing)
|
||||
if (platform === 'linux') {
|
||||
console.log(`Native: Launching LibreOffice on Linux for path: ${cleaned_path}`);
|
||||
const cmd = `libreoffice --impress "${cleaned_path}"`;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
child_process.exec(cmd, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error('LibreOffice Launch Error:', err);
|
||||
resolve({ success: false, error: err.message });
|
||||
} else {
|
||||
resolve({ success: true, stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Handle macOS (Production)
|
||||
if (platform === 'darwin') {
|
||||
if (app === 'keynote') {
|
||||
const script = `tell application "Keynote" to open POSIX file "${cleaned_path}"`;
|
||||
return exports.run_osascript({ cmd: script });
|
||||
}
|
||||
|
||||
// Default to shell open
|
||||
return ipcRenderer.invoke('open_local_file', '', cleaned_path);
|
||||
}
|
||||
|
||||
// 4. Default Fallback (Windows/Others)
|
||||
return ipcRenderer.invoke('open_local_file', '', cleaned_path);
|
||||
};
|
||||
|
||||
// For loading JS file
|
||||
function loadJS() {
|
||||
// Gives -1 when the given input is not in the string
|
||||
|
||||
Reference in New Issue
Block a user