Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/ci-changes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function classifyChanges(paths) {
else if (path.startsWith('windows/')) checks.windows = true
else if (path.startsWith('linux/') || /scripts\/(build|smoke-test)-linux/.test(path)) checks.linux = true
else if (path.startsWith('macos/') || /scripts\/(build|package)-macos/.test(path)) Object.assign(checks, { rust: true, macos: true })
else if (/^scripts\/dev-slot(\.test)?\.mjs$/.test(path)) checks.dev_slot = true
else if (/^scripts\/(?:dev-slot(?:\.test)?\.mjs|desktop\.js)$/.test(path)) checks.dev_slot = true
else if (/^scripts\/(test_orx_chart|orx_chart)\.py$/.test(path)) checks.chart = true
else if (/^scripts\/(test_release_notes|release_notes)\.py$/.test(path)) checks.release_notes = true
else return all()
Expand Down
1 change: 1 addition & 0 deletions scripts/ci-changes.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ test('platform files select corresponding checks', () => {
assert.deepEqual(enabled(['macos/Info.plist']), ['rust', 'macos'])
})
test('helper tests are independent and combine with other changes', () => {
assert.deepEqual(enabled(['scripts/desktop.js']), ['dev_slot'])
assert.deepEqual(enabled(['scripts/dev-slot.mjs', 'scripts/test_orx_chart.py', 'scripts/release_notes.py', 'ui/package.json']), ['frontend', 'dev_slot', 'chart', 'release_notes'])
})
test('docs skip builds, workflow changes and unknown files select all', () => {
Expand Down
6 changes: 6 additions & 0 deletions scripts/desktop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import('./dev-slot.mjs')
.then(({ desktop }) => desktop())
.catch((error) => {
console.error(`desktop: ${error.message}`)
process.exitCode = 1
})
111 changes: 91 additions & 20 deletions scripts/dev-slot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ function saveState(slotPaths, state) {
atomicWriteJson(slotPaths.statePath, state)
}

async function startUnlocked(info, dbMode, openBrowser, desktop) {
async function startUnlocked(info, dbMode, openBrowser) {
const reservation = await reserveSlot(info, dbMode)
const { slotPaths } = reservation
const state = reservation.state
Expand Down Expand Up @@ -668,22 +668,6 @@ async function startUnlocked(info, dbMode, openBrowser, desktop) {
state.phase = 'running'
saveState(slotPaths, state)

if (desktop) {
execFileSync('cargo', ['build', '--manifest-path', info.manifestPath], {
cwd: info.worktreePath, env: { ...process.env, ...slotEnvironment(slotPaths) }, stdio: 'inherit',
})
const contents = path.join(slotPaths.cacheDir, 'OpenResearch Dev.app', 'Contents')
mkdirSync(path.join(contents, 'MacOS'), { recursive: true })
const executable = path.join(contents, 'MacOS', 'OpenResearch Dev')
cpSync(path.join(slotPaths.cargoTargetDir, 'debug', 'orx'), executable)
writeFileSync(path.join(contents, 'Info.plist'), `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>CFBundleExecutable</key><string>OpenResearch Dev</string><key>CFBundleIdentifier</key><string>org.alphaxiv.openresearch.dev.slot${slotPaths.slot}</string><key>CFBundleName</key><string>OpenResearch Dev</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist>\n`)
const child = spawn(executable, ['--dev-desktop', String(slotPaths.uiPort)], {
detached: true, stdio: 'ignore', env: { ...process.env, ...slotEnvironment(slotPaths) },
})
child.on('error', (error) => console.error(`Could not open the desktop app: ${error.message}`))
child.unref()
}

if (openBrowser) {
const url = `http://localhost:${slotPaths.uiPort}/`
const opener = process.platform === 'darwin'
Expand All @@ -704,15 +688,101 @@ async function startUnlocked(info, dbMode, openBrowser, desktop) {
console.log(` State: ${slotPaths.statePath}`)
}

async function start(info, dbMode, openBrowser, desktop) {
async function start(info, dbMode, openBrowser) {
const release = await acquireLifecycleLock(info)
try {
await startUnlocked(info, dbMode, openBrowser, desktop)
await startUnlocked(info, dbMode, openBrowser)
} finally {
await release()
}
}

export async function desktop(worktree = process.cwd(), requestedMode = null) {
if (process.platform !== 'darwin') throw new Error('The development desktop app requires macOS')
const info = worktreeInfo(worktree)
let child = null
let exit = null
let result = null
let record = null
let started = false
let interrupted = false
const interrupt = () => {
interrupted = true
if (child?.pid && processGroupExists(child.pid)) {
try { process.kill(-child.pid, 'SIGTERM') } catch (error) { if (error.code !== 'ESRCH') throw error }
}
}
process.on('SIGINT', interrupt)
process.on('SIGTERM', interrupt)
try {
const release = await acquireLifecycleLock(info)
try {
const previous = reservationFor(info)
if (previous?.state?.desktopProcess && processGroupExists(previous.state.desktopProcess.pid)) {
if (!supervisorMatches(previous.state.desktopProcess)) throw new Error('The previous desktop process no longer matches its recorded identity')
process.kill(-previous.state.desktopProcess.pid, 'SIGTERM')
await waitFor('the previous desktop app to close', 10_000, () => !processGroupExists(previous.state.desktopProcess.pid))
}
await stopOwnedProcessesUnlocked(info)
if (interrupted) return
const mode = requestedMode ?? previous?.state?.dbMode ?? 'empty'
started = true
await startUnlocked(info, mode, false)
Comment on lines +720 to +730

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failed launch stops working session

With a running copy slot, start --db empty --desktop now kills the desktop app and stops both servers before reserveSlot rejects the database-mode mismatch. Previously, the rejected command left the running slot alone.

Check the requested mode before stopping the existing app and servers so a failed launch does not interrupt the developer's session.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/dev-slot.mjs
Line: 720-730

Comment:
**Failed launch stops working session**

With a running `copy` slot, `start --db empty --desktop` now kills the desktop app and stops both servers before `reserveSlot` rejects the database-mode mismatch. Previously, the rejected command left the running slot alone.

Check the requested mode before stopping the existing app and servers so a failed launch does not interrupt the developer's session.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

if (interrupted) return
const { slotPaths, state } = reservationFor(info)
execFileSync('cargo', ['build', '--manifest-path', info.manifestPath], {
cwd: info.worktreePath, env: { ...process.env, ...slotEnvironment(slotPaths) }, stdio: 'inherit',
})
if (interrupted) return
const contents = path.join(slotPaths.cacheDir, 'OpenResearch Dev.app', 'Contents')
mkdirSync(path.join(contents, 'MacOS'), { recursive: true })
const executable = path.join(contents, 'MacOS', 'OpenResearch Dev')
cpSync(path.join(slotPaths.cargoTargetDir, 'debug', 'orx'), executable)
writeFileSync(path.join(contents, 'Info.plist'), `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>CFBundleExecutable</key><string>OpenResearch Dev</string><key>CFBundleIdentifier</key><string>org.alphaxiv.openresearch.dev.slot${slotPaths.slot}</string><key>CFBundleName</key><string>OpenResearch Dev</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist>\n`)
const token = randomUUID()
child = spawn(process.execPath, [SCRIPT_PATH, '_supervise', token, executable, '--dev-desktop', String(slotPaths.uiPort)], {
detached: true, stdio: 'inherit', env: { ...process.env, ...slotEnvironment(slotPaths) },
})
if (!child.pid) throw new Error('Could not launch the desktop app')
exit = new Promise((resolve, reject) => {
child.once('error', reject)
child.once('exit', (code, signal) => resolve({ code, signal }))
})
record = { pid: child.pid, token, scriptPath: SCRIPT_PATH }
state.desktopProcess = record
saveState(slotPaths, state)
console.log('Close the desktop window to stop the development servers.')
} finally {
try {
if (started && !record) await stopOwnedProcessesUnlocked(info)
} finally {
await release()
}
}
if (exit) {
result = await exit
}
} finally {
process.removeListener('SIGINT', interrupt)
process.removeListener('SIGTERM', interrupt)
const release = await acquireLifecycleLock(info)
try {
const current = reservationFor(info)
if (current && record && current.state?.desktopProcess?.token === record.token) {
if (!interrupted && result && (result.signal || result.code !== 0)) process.exitCode = 1
await stopOwnedProcessesUnlocked(info)
const state = readJson(current.slotPaths.statePath)
if (state) {
state.desktopProcess = null
saveState(current.slotPaths, state)
}
}
} finally {
await release()
}
}
}

function reservationFor(info) {
const registry = loadRegistry(info)
const backend = registry.configurations.find((configuration) =>
Expand Down Expand Up @@ -881,7 +951,8 @@ async function main() {
}
const options = parseArgs(process.argv.slice(2))
const info = worktreeInfo(options.worktree)
if (options.command === 'start') await start(info, options.db, options.open, options.desktop)
if (options.command === 'start' && options.desktop) await desktop(options.worktree, options.db)
else if (options.command === 'start') await start(info, options.db, options.open)
if (options.command === 'status') printStatus(info)
if (options.command === 'stop') await stopOwnedProcesses(info)
if (options.command === 'cleanup') await cleanup(info)
Expand Down
8 changes: 6 additions & 2 deletions src/commands/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1103,8 +1103,12 @@ mod imp {
..
} => {
#[cfg(target_os = "macos")]
// AppKit switches Spaces when hiding the app, unlike ordering out its window.
NSApplication::sharedApplication(MainThreadMarker::new().unwrap()).hide(None);
if external_server {
begin_quit(&mut quit, &window, &webview, control_flow);
} else {
// AppKit switches Spaces when hiding the app, unlike ordering out its window.
NSApplication::sharedApplication(MainThreadMarker::new().unwrap()).hide(None);
}
#[cfg(not(target_os = "macos"))]
begin_quit(&mut quit, &window, &webview, control_flow);
}
Expand Down
12 changes: 12 additions & 0 deletions src/commands/up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ impl ProjectLifecycle {
fn router(state: AppState, remote_auth: Option<RemoteAuth>) -> Router {
let app = Router::new()
.route("/api/health", get(health))
.route("/api/file-manager", get(file_manager))
.route("/api/onboarding/complete", post(complete_onboarding))
.route("/api/project-path/status", get(project_path_status))
.route("/api/project-path/pick", post(pick_project_folder))
Expand Down Expand Up @@ -1012,6 +1013,17 @@ async fn health(State(state): State<AppState>) -> Json<Value> {
}))
}

async fn file_manager(
Query(query): Query<std::collections::HashMap<String, String>>,
) -> Json<Value> {
let locale = query.get("locale").cloned().unwrap_or_default();
let name = tokio::task::spawn_blocking(move || crate::editors::file_manager_name(&locale))
.await
.ok()
.flatten();
Json(json!({ "os": std::env::consts::OS, "name": name }))
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CompleteOnboardingReq {
Expand Down
97 changes: 97 additions & 0 deletions src/editors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,107 @@ pub fn reveal_in_file_manager(path: &std::path::Path) -> std::io::Result<()> {
spawn_detached(&mut reveal_command(path))
}

/// The directory handler named by the platform, if it can be identified.
pub fn file_manager_name(_locale: &str) -> Option<String> {
#[cfg(target_os = "macos")]
{
None
}
#[cfg(target_os = "windows")]
{
None
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
let mut command = Command::new("xdg-mime");
command.args(["query", "default", "inode/directory"]);
crate::local::shell_env::restore_host_gui_env(&mut command);
let output = command.output().ok()?;
if !output.status.success() {
return None;
}
let id = String::from_utf8(output.stdout).ok()?;
let id = id.trim();
if id.is_empty() || id.contains(['/', '\\']) {
return None;
}
let home = std::env::var_os("HOME").map(std::path::PathBuf::from);
let data_home = std::env::var_os("XDG_DATA_HOME")
.map(std::path::PathBuf::from)
.or_else(|| home.map(|path| path.join(".local/share")));
let dirs = crate::local::shell_env::host_gui_env()
.into_iter()
.find(|(key, _)| *key == "XDG_DATA_DIRS")
.map(|(_, value)| value)
.unwrap_or_else(|| std::env::var_os("XDG_DATA_DIRS"))
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "/usr/local/share:/usr/share".into());
data_home
.into_iter()
.chain(std::env::split_paths(&dirs))
.find_map(|path| {
std::fs::read_to_string(path.join("applications").join(id))
.ok()
.and_then(|entry| desktop_entry_name(&entry, _locale))
})
}
}

#[cfg(any(test, not(any(target_os = "macos", target_os = "windows"))))]
fn desktop_entry_name(entry: &str, locale: &str) -> Option<String> {
let locale = locale.replace('-', "_");
let language = locale.split('_').next().unwrap_or(&locale);
let mut in_entry = false;
let mut default = None;
let mut localized = None;
for line in entry.lines().map(str::trim) {
if line.starts_with('[') && line.ends_with(']') {
in_entry = line == "[Desktop Entry]";
} else if in_entry {
let Some((key, name)) = line.split_once('=') else {
continue;
};
let name = name.trim();
if name.is_empty() {
continue;
}
if key == "Name" {
default = Some(name);
} else if key == format!("Name[{locale}]") {
return Some(name.to_owned());
} else if key == format!("Name[{language}]") {
localized = Some(name);
}
}
}
localized.or(default).map(str::to_owned)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn desktop_entry_name_uses_the_application_section() {
assert_eq!(
desktop_entry_name(
"[Other]\nName=Wrong\n[Desktop Entry]\nName=Files\n[Desktop Action New]\nName=New",
"en"
),
Some("Files".into())
);
assert_eq!(desktop_entry_name("[Desktop Entry]\nName=\n", "en"), None);
let entry =
"[Desktop Entry]\nName=Files\nName[zh]=文件\nName[zh_CN]=文件管理器\nName[es]=Archivos";
assert_eq!(
desktop_entry_name(entry, "zh-CN"),
Some("文件管理器".into())
);
assert_eq!(desktop_entry_name(entry, "zh-TW"), Some("文件".into()));
assert_eq!(desktop_entry_name(entry, "es"), Some("Archivos".into()));
assert_eq!(desktop_entry_name(entry, "ja"), Some("Files".into()));
}

fn args(cmd: &Command) -> Vec<String> {
cmd.get_args()
.map(|a| a.to_string_lossy().into_owned())
Expand Down
4 changes: 3 additions & 1 deletion ui/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,9 @@
"sidebar_source_folder": "مجلد المصدر",
"sidebar_pin_project": "تثبيت",
"sidebar_unpin_project": "إلغاء التثبيت",
"sidebar_reveal_project": "إظهار في Finder",
"file_manager_finder": "Finder",
"file_manager_explorer": "مستكشف الملفات",
"reveal_in_named_file_manager": "إظهار في {manager}",
"sidebar_filter": "تصفية",
"sidebar_group_by": "تجميع حسب",
"sidebar_one_list": "في قائمة واحدة",
Expand Down
4 changes: 3 additions & 1 deletion ui/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,9 @@
"sidebar_source_folder": "Source folder",
"sidebar_pin_project": "Pin",
"sidebar_unpin_project": "Unpin",
"sidebar_reveal_project": "Reveal in Finder",
"file_manager_finder": "Finder",
"file_manager_explorer": "File Explorer",
"reveal_in_named_file_manager": "Reveal in {manager}",
"sidebar_filter": "Filter",
"sidebar_group_by": "Group by",
"sidebar_one_list": "In one list",
Expand Down
4 changes: 3 additions & 1 deletion ui/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,9 @@
"sidebar_source_folder": "Carpeta de origen",
"sidebar_pin_project": "Fijar",
"sidebar_unpin_project": "Desfijar",
"sidebar_reveal_project": "Mostrar en Finder",
"file_manager_finder": "Finder",
"file_manager_explorer": "Explorador de archivos",
"reveal_in_named_file_manager": "Mostrar en {manager}",
"sidebar_filter": "Filtrar",
"sidebar_group_by": "Agrupar por",
"sidebar_one_list": "En una lista",
Expand Down
4 changes: 3 additions & 1 deletion ui/messages/fa.json
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,9 @@
"sidebar_source_folder": "پوشهٔ منبع",
"sidebar_pin_project": "سنجاق کردن",
"sidebar_unpin_project": "برداشتن سنجاق",
"sidebar_reveal_project": "نمایش در Finder",
"file_manager_finder": "Finder",
"file_manager_explorer": "کاوشگر فایل",
"reveal_in_named_file_manager": "نمایش در {manager}",
"sidebar_filter": "فیلتر",
"sidebar_group_by": "گروه‌بندی بر اساس",
"sidebar_one_list": "در یک فهرست",
Expand Down
4 changes: 3 additions & 1 deletion ui/messages/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,9 @@
"sidebar_source_folder": "स्रोत फ़ोल्डर",
"sidebar_pin_project": "पिन करें",
"sidebar_unpin_project": "पिन हटाएँ",
"sidebar_reveal_project": "Finder में दिखाएँ",
"file_manager_finder": "Finder",
"file_manager_explorer": "फ़ाइल एक्सप्लोरर",
"reveal_in_named_file_manager": "{manager} में दिखाएँ",
"sidebar_filter": "फ़िल्टर",
"sidebar_group_by": "समूह बनाएँ",
"sidebar_one_list": "एक सूची में",
Expand Down
4 changes: 3 additions & 1 deletion ui/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,9 @@
"sidebar_source_folder": "ソースフォルダー",
"sidebar_pin_project": "ピン留め",
"sidebar_unpin_project": "ピン留めを解除",
"sidebar_reveal_project": "Finder で表示",
"file_manager_finder": "Finder",
"file_manager_explorer": "エクスプローラー",
"reveal_in_named_file_manager": "{manager} で表示",
"sidebar_filter": "フィルター",
"sidebar_group_by": "グループ分け",
"sidebar_one_list": "1 つの一覧",
Expand Down
4 changes: 3 additions & 1 deletion ui/messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,9 @@
"sidebar_source_folder": "소스 폴더",
"sidebar_pin_project": "고정",
"sidebar_unpin_project": "고정 해제",
"sidebar_reveal_project": "Finder에서 표시",
"file_manager_finder": "Finder",
"file_manager_explorer": "파일 탐색기",
"reveal_in_named_file_manager": "{manager}에서 표시",
"sidebar_filter": "필터",
"sidebar_group_by": "그룹화 기준",
"sidebar_one_list": "단일 목록",
Expand Down
Loading
Loading