feat(launcher): configurable launch scripts + composable native primitives
- electron_relay: type launch_from_cache with script_template param; add copy_from_cache_to_temp export; add JSDoc for run_osascript hardening - launcher_file_cont: add get_launch_script_template() helper reading from device-level (event_device.data_json.launch_scripts) and event-level (events_loc.launcher.launch_scripts) config; wire into handle_open_file() - PROJECT__AE_Events_Launcher_Native_integration.md: add Section 8 (Configurable Launch Scripts); update IPC reference for new/changed handlers - MODULE__AE_Events_PressMgmt_Launcher.md: add configurable launch behavior note to Native Mode Safe Handover section
This commit is contained in:
@@ -311,6 +311,13 @@ The Electron app zero-configs itself:
|
|||||||
3. Rename to original filename (e.g., `Abstract_101.pptx`)
|
3. Rename to original filename (e.g., `Abstract_101.pptx`)
|
||||||
4. OS opens the file (Keynote, PowerPoint, Preview, etc.)
|
4. OS opens the file (Keynote, PowerPoint, Preview, etc.)
|
||||||
|
|
||||||
|
**Configurable launch behavior:** The open/launch command in step 4 can be overridden
|
||||||
|
per file extension via `event_device.data_json.launch_scripts` (device-level config) or
|
||||||
|
`event.launcher.launch_scripts` (event-level fallback). Templates use `{{path}}` as the
|
||||||
|
file path placeholder; AppleScript or `shell:` prefixed commands are both supported. No
|
||||||
|
Electron rebuild required to change how files open — edit config in Aether and it applies
|
||||||
|
immediately. See `PROJECT__AE_Events_Launcher_Native_integration.md` Section 8.
|
||||||
|
|
||||||
Versioning is handled automatically: when a presenter uploads an updated file, the new
|
Versioning is handled automatically: when a presenter uploads an updated file, the new
|
||||||
hash is cached separately and the old one remains intact.
|
hash is cached separately and the old one remains intact.
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ no-op when `window.aetherNative` is not present (i.e., in browser/non-native mod
|
|||||||
### File Cache
|
### File Cache
|
||||||
- `check_hash_file_cache({cache_root, hash, hash_prefix_length?})` — Verifies a file exists in the local hashed cache.
|
- `check_hash_file_cache({cache_root, hash, hash_prefix_length?})` — Verifies a file exists in the local hashed cache.
|
||||||
- `download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?})` — Streams a file download to the hashed cache with SHA-256 integrity check.
|
- `download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?})` — Streams a file download to the hashed cache with SHA-256 integrity check.
|
||||||
- `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?})` — Atomic "Safe Handover": copy from cache → tmp → rename → execute.
|
- `copy_from_cache_to_temp({cache_root, hash, temp_root, filename, hash_prefix_length?})` — **Preferred primitive.** Copies a cached file to temp and returns `{ success, path }`. The Svelte caller decides what to do next (run a script, open it, etc.).
|
||||||
|
- `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?, script_template?})` — Combines copy + launch in one call. Uses `script_template` if provided, otherwise falls back to hardcoded extension logic. See **Configurable Launch Scripts** below.
|
||||||
- `cleanup_tmp_files({cache_root, max_age_minutes?})` — Removes stale `*.tmp` download artifacts. Default: 1440 min (24h). Called at launcher startup.
|
- `cleanup_tmp_files({cache_root, max_age_minutes?})` — Removes stale `*.tmp` download artifacts. Default: 1440 min (24h). Called at launcher startup.
|
||||||
|
|
||||||
> `hash_prefix_length` defaults to `2` throughout. Do not change without coordinating all devices — mismatched values create orphaned cache subdirectories.
|
> `hash_prefix_length` defaults to `2` throughout. Do not change without coordinating all devices — mismatched values create orphaned cache subdirectories.
|
||||||
@@ -153,8 +154,8 @@ no-op when `window.aetherNative` is not present (i.e., in browser/non-native mod
|
|||||||
- `open_folder(path)` — Opens a path in the OS file manager.
|
- `open_folder(path)` — Opens a path in the OS file manager.
|
||||||
- `run_cmd({cmd, timeout?, return_stdout?})` — Async shell command execution.
|
- `run_cmd({cmd, timeout?, return_stdout?})` — Async shell command execution.
|
||||||
- `run_cmd_sync({cmd, return_stdout?})` — Synchronous shell command execution.
|
- `run_cmd_sync({cmd, return_stdout?})` — Synchronous shell command execution.
|
||||||
- `run_osascript(script)` — Executes an AppleScript string. macOS only.
|
- `run_osascript(script)` — Executes an AppleScript string. macOS only. **Hardened (2026-05-11):** writes script to a temp `.scpt` file; multi-line scripts and paths with special characters now work correctly. No shell escaping needed in the passed string.
|
||||||
- `kill_processes({process_name_li})` — Gracefully terminates processes by name.
|
- `kill_processes({process_name_li})` — Terminates processes by name. macOS/Linux: `pkill -f`. Windows: `taskkill /F`.
|
||||||
- `open_local_file_v2(path)` — Opens a file with its default OS application.
|
- `open_local_file_v2(path)` — Opens a file with its default OS application.
|
||||||
|
|
||||||
### Presentations (Phase 5)
|
### Presentations (Phase 5)
|
||||||
@@ -176,5 +177,64 @@ All paths passed to native handlers should use tokens rather than hardcoded OS p
|
|||||||
- `[home]` — Resolved to the user's home directory by the native bridge.
|
- `[home]` — Resolved to the user's home directory by the native bridge.
|
||||||
- `[tmp]` — Resolved to the system temporary directory.
|
- `[tmp]` — Resolved to the system temporary directory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Configurable Launch Scripts (No-Rebuild File Handling)
|
||||||
|
|
||||||
|
To avoid requiring a full Electron rebuild for changes to how files are opened, `launch_from_cache`
|
||||||
|
supports an optional `script_template` parameter. When provided, Electron runs the template
|
||||||
|
instead of its built-in hardcoded logic. The hardcoded logic remains intact as the fallback
|
||||||
|
when no template is configured.
|
||||||
|
|
||||||
|
### Template Formats
|
||||||
|
|
||||||
|
| Format | Example |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **AppleScript** (macOS) | Multi-line AppleScript string with `{{path}}` placeholder |
|
||||||
|
| **Shell command** | String prefixed with `shell:` — e.g. `shell:open "{{path}}"` |
|
||||||
|
|
||||||
|
The placeholder `{{path}}` is replaced with the full resolved path to the file in the
|
||||||
|
temp directory (after the atomic copy from cache).
|
||||||
|
|
||||||
|
### Where to Configure
|
||||||
|
|
||||||
|
Templates are resolved in priority order by `get_launch_script_template()` in
|
||||||
|
`launcher_file_cont.svelte`:
|
||||||
|
|
||||||
|
1. **`event_device.data_json.launch_scripts`** — API-driven, per-device. Highest priority.
|
||||||
|
Set via the `event_device` record (Pres Mgmt → Device Management or direct DB edit).
|
||||||
|
2. **`$events_loc.launcher.launch_scripts`** — Local persistent config. Editable via the
|
||||||
|
Launcher config UI (planned) or direct `localStorage` manipulation.
|
||||||
|
|
||||||
|
If neither is set, `script_template` is `null` and Electron uses its built-in hardcoded defaults.
|
||||||
|
|
||||||
|
### Key Format
|
||||||
|
|
||||||
|
Keys are lowercase file extensions without the dot. A `"default"` key catches all
|
||||||
|
unrecognised extensions.
|
||||||
|
|
||||||
|
```json
|
||||||
|
// event_device.data_json.launch_scripts example
|
||||||
|
{
|
||||||
|
"launch_scripts": {
|
||||||
|
"pptx": "tell application \"Microsoft PowerPoint\"\n activate\n open (POSIX file \"{{path}}\")\n delay 3\nend tell\ntell application \"System Events\"\n keystroke return using command down\nend tell",
|
||||||
|
"key": "tell application \"Keynote\"\n activate\n open (POSIX file \"{{path}}\")\n delay 1\n start (front document)\nend tell",
|
||||||
|
"pdf": "shell:open \"{{path}}\"",
|
||||||
|
"default": "shell:open \"{{path}}\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Known Issue: `launch_presentation` vs `launch_from_cache` Inconsistency
|
||||||
|
|
||||||
|
`shell_handlers.ts` `native:launch-presentation` still uses the old `osascript -e "<inline>"` approach
|
||||||
|
for its AppleScript execution. `file_handlers.ts` `native:launch-from-cache` uses the hardened
|
||||||
|
temp-`.scpt`-file approach. These two handlers behave differently for identical file types.
|
||||||
|
|
||||||
|
- **`launch_from_cache`** (used by the "Open" button in the Launcher file list) — hardened, correct.
|
||||||
|
- **`launch_presentation`** (used by `electron_relay.launch_presentation`) — legacy `-e` flag, fragile on paths with spaces or special characters.
|
||||||
|
|
||||||
|
**Recommendation:** `launch_presentation` should be updated to use the temp-`.scpt` approach in a future Electron build. It is not used in the primary file-open flow today, so this is not blocking.
|
||||||
|
|
||||||
### Not Exposed via Relay (intentional)
|
### Not Exposed via Relay (intentional)
|
||||||
- `get_seed_config` / `get_jwt` — Exposed in the preload but not relayed to the UI. The JWT and seed are injected into the environment at startup; components should not call these directly.
|
- `get_seed_config` / `get_jwt` — Exposed in the preload but not relayed to the UI. The JWT and seed are injected into the environment at startup; components should not call these directly.
|
||||||
|
|||||||
@@ -64,11 +64,68 @@ export async function launch_from_cache({
|
|||||||
hash,
|
hash,
|
||||||
temp_root,
|
temp_root,
|
||||||
filename,
|
filename,
|
||||||
hash_prefix_length = 2
|
hash_prefix_length = 2,
|
||||||
}: any) {
|
script_template = null
|
||||||
|
}: {
|
||||||
|
cache_root: string;
|
||||||
|
hash: string;
|
||||||
|
temp_root: string;
|
||||||
|
filename: string;
|
||||||
|
hash_prefix_length?: number;
|
||||||
|
/**
|
||||||
|
* Optional data-driven launch script. If provided, Electron runs this instead of
|
||||||
|
* its hardcoded extension-based logic — no app rebuild needed for script changes.
|
||||||
|
*
|
||||||
|
* Two formats:
|
||||||
|
* - AppleScript: multi-line string with {{path}} placeholder (macOS only)
|
||||||
|
* - Shell command: prefix with "shell:" → e.g. "shell:open \"{{path}}\""
|
||||||
|
*
|
||||||
|
* Configure via event_device.data_json.launch_scripts or $events_loc.launcher.launch_scripts.
|
||||||
|
* If null, Electron falls through to its built-in hardcoded defaults.
|
||||||
|
*/
|
||||||
|
script_template?: string | null;
|
||||||
|
}) {
|
||||||
if (!native)
|
if (!native)
|
||||||
return { success: false, error: 'Native bridge not available' };
|
return { success: false, error: 'Native bridge not available' };
|
||||||
return await native.launch_from_cache({
|
return await native.launch_from_cache({
|
||||||
|
cache_root,
|
||||||
|
hash,
|
||||||
|
temp_root,
|
||||||
|
filename,
|
||||||
|
hash_prefix_length,
|
||||||
|
script_template
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin cache primitive — copies a cached file to the temp directory and returns
|
||||||
|
* the resolved path. The caller decides what happens next.
|
||||||
|
*
|
||||||
|
* Preferred building block for composable launch flows on the Svelte side:
|
||||||
|
* 1. copy_from_cache_to_temp(...) → { path }
|
||||||
|
* 2. run_osascript(template.replace('{{path}}', path))
|
||||||
|
* OR run_cmd(`open "${path}"`)
|
||||||
|
* OR whatever you need
|
||||||
|
*
|
||||||
|
* Use launch_from_cache when the built-in hardcoded logic is sufficient.
|
||||||
|
* Use this when you want full control over what happens after the file lands in temp.
|
||||||
|
*/
|
||||||
|
export async function copy_from_cache_to_temp({
|
||||||
|
cache_root,
|
||||||
|
hash,
|
||||||
|
temp_root,
|
||||||
|
filename,
|
||||||
|
hash_prefix_length = 2
|
||||||
|
}: {
|
||||||
|
cache_root: string;
|
||||||
|
hash: string;
|
||||||
|
temp_root: string;
|
||||||
|
filename: string;
|
||||||
|
hash_prefix_length?: number;
|
||||||
|
}): Promise<{ success: boolean; path?: string; error?: string }> {
|
||||||
|
if (!native)
|
||||||
|
return { success: false, error: 'Native bridge not available' };
|
||||||
|
return await native.copy_from_cache_to_temp({
|
||||||
cache_root,
|
cache_root,
|
||||||
hash,
|
hash,
|
||||||
temp_root,
|
temp_root,
|
||||||
@@ -129,6 +186,18 @@ export async function cleanup_tmp_files({
|
|||||||
return await native.run_cmd({ cmd, timeout: 30000, return_stdout: false });
|
return await native.run_cmd({ cmd, timeout: 30000, return_stdout: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes an AppleScript string. macOS only.
|
||||||
|
*
|
||||||
|
* HARDENED (2026-05-11): The Electron handler now writes the script to a temp .scpt
|
||||||
|
* file and runs `osascript "/path/to/file.scpt"` rather than passing it inline via
|
||||||
|
* the -e flag. This means:
|
||||||
|
* - Multi-line scripts work correctly
|
||||||
|
* - Paths with spaces or special characters work correctly
|
||||||
|
* - No shell escaping required in the script string you pass here
|
||||||
|
*
|
||||||
|
* The .scpt file is deleted immediately after execution.
|
||||||
|
*/
|
||||||
export async function run_osascript(script: string) {
|
export async function run_osascript(script: string) {
|
||||||
if (!native)
|
if (!native)
|
||||||
return { success: false, error: 'Native bridge not available' };
|
return { success: false, error: 'Native bridge not available' };
|
||||||
|
|||||||
@@ -88,6 +88,41 @@ let open_file_status_message: null | string = $state(null);
|
|||||||
|
|
||||||
let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp'];
|
let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a data-driven launch script template for a given file extension.
|
||||||
|
* Checked in priority order:
|
||||||
|
* 1. event_device.data_json.launch_scripts (API-driven, per-device, most specific)
|
||||||
|
* 2. $events_loc.launcher.launch_scripts (local persistent override)
|
||||||
|
* Keys are lowercase extensions without the dot ("pptx", "key", "pdf", etc.).
|
||||||
|
* A "default" key acts as a catch-all for unrecognised extensions.
|
||||||
|
*
|
||||||
|
* Returns null when no config is found → Electron falls back to its hardcoded defaults.
|
||||||
|
*
|
||||||
|
* Template formats:
|
||||||
|
* - AppleScript (macOS): plain string with {{path}} placeholder
|
||||||
|
* - Shell command: prefix with "shell:" → "shell:open \"{{path}}\""
|
||||||
|
*/
|
||||||
|
function get_launch_script_template(extension: string): string | null {
|
||||||
|
const ext = (extension || '').toLowerCase().replace('.', '');
|
||||||
|
|
||||||
|
// 1. Device-level config (from API, per device — highest priority)
|
||||||
|
const device_scripts = ($ae_loc as any).native_device?.launch_scripts;
|
||||||
|
if (device_scripts) {
|
||||||
|
if (device_scripts[ext]) return device_scripts[ext];
|
||||||
|
if (device_scripts['default']) return device_scripts['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Launcher local config override (set manually via Launcher config UI)
|
||||||
|
const local_scripts = ($events_loc as any).launcher?.launch_scripts;
|
||||||
|
if (local_scripts) {
|
||||||
|
if (local_scripts[ext]) return local_scripts[ext];
|
||||||
|
if (local_scripts['default']) return local_scripts['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. No override — let Electron use its built-in hardcoded defaults
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (screen_saver_exts.includes(event_file_obj.extension)) {
|
if (screen_saver_exts.includes(event_file_obj.extension)) {
|
||||||
if (!$events_loc.launcher.screen_saver_img_kv)
|
if (!$events_loc.launcher.screen_saver_img_kv)
|
||||||
@@ -149,11 +184,14 @@ async function handle_open_file() {
|
|||||||
// Phase 2/5: Use the atomic copy-and-launch operation.
|
// Phase 2/5: Use the atomic copy-and-launch operation.
|
||||||
// The main process handler (file_handlers.ts) now handles the
|
// The main process handler (file_handlers.ts) now handles the
|
||||||
// specialized LibreOffice/AppleScript logic internally after copying.
|
// specialized LibreOffice/AppleScript logic internally after copying.
|
||||||
|
// script_template is null when no device/local config exists → Electron uses hardcoded defaults.
|
||||||
|
const script_template = get_launch_script_template(event_file_obj.extension);
|
||||||
const launch_result = await native.launch_from_cache({
|
const launch_result = await native.launch_from_cache({
|
||||||
cache_root,
|
cache_root,
|
||||||
hash: event_file_obj.hash_sha256,
|
hash: event_file_obj.hash_sha256,
|
||||||
temp_root,
|
temp_root,
|
||||||
filename: event_file_obj.filename
|
filename: event_file_obj.filename,
|
||||||
|
script_template
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!launch_result.success) {
|
if (!launch_result.success) {
|
||||||
|
|||||||
Reference in New Issue
Block a user