Add native self-hosted instance connection to fluxer_desktop
Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use napi::{Error, Result, Status};
|
||||
use napi_derive::napi;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use sysctl::{Ctl, CtlValue, Sysctl, SysctlError};
|
||||
|
||||
fn invalid_name_error(function_name: &str) -> Error {
|
||||
Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{function_name} requires a non-empty name string"),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn map_sysctl_error(error: SysctlError) -> Error {
|
||||
match error {
|
||||
SysctlError::IoError(ref io_error) => {
|
||||
if let Some(errno) = io_error.raw_os_error() {
|
||||
return Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("sysctlbyname failed (errno {errno})"),
|
||||
);
|
||||
}
|
||||
}
|
||||
SysctlError::MissingImplementation
|
||||
| SysctlError::ExtractionError
|
||||
| SysctlError::ParseError
|
||||
| SysctlError::InvalidCStr(_)
|
||||
| SysctlError::InvalidCString(_) => {
|
||||
return Error::new(
|
||||
Status::GenericFailure,
|
||||
"sysctlbyname failed (errno 22)".to_owned(),
|
||||
);
|
||||
}
|
||||
SysctlError::NotFound(_)
|
||||
| SysctlError::Utf8Error(_)
|
||||
| SysctlError::NoReadAccess
|
||||
| SysctlError::NoWriteAccess
|
||||
| SysctlError::NotSupported
|
||||
| SysctlError::ShortRead { .. } => {}
|
||||
}
|
||||
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("sysctlbyname failed ({error})"),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn unsupported_platform_error() -> Error {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
"sysctlbyname failed (errno 22)".to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn ctl_for_name(name: &str) -> Result<Option<Ctl>> {
|
||||
match Ctl::new(name) {
|
||||
Ok(ctl) => Ok(Some(ctl)),
|
||||
Err(SysctlError::NotFound(_)) => Ok(None),
|
||||
Err(error) => Err(map_sysctl_error(error)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn value_to_number(value: CtlValue) -> Result<Option<f64>> {
|
||||
let number = match value {
|
||||
CtlValue::None => return Ok(None),
|
||||
CtlValue::Int(value) => f64::from(value),
|
||||
CtlValue::Uint(value) => f64::from(value),
|
||||
CtlValue::Long(value) => value as f64,
|
||||
CtlValue::Ulong(value) => value as f64,
|
||||
CtlValue::S64(value) => value as f64,
|
||||
CtlValue::U64(value) => value as f64,
|
||||
CtlValue::S32(value) => f64::from(value),
|
||||
CtlValue::U32(value) => f64::from(value),
|
||||
CtlValue::S16(value) => f64::from(value),
|
||||
CtlValue::U16(value) => f64::from(value),
|
||||
CtlValue::S8(value) => f64::from(value),
|
||||
CtlValue::U8(value) => f64::from(value),
|
||||
CtlValue::String(_) | CtlValue::Struct(_) | CtlValue::Node(_) => {
|
||||
return Err(Error::new(
|
||||
Status::GenericFailure,
|
||||
"sysctlbyname failed (errno 22)".to_owned(),
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(Some(number))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read_int_blocking(name: String) -> Result<Option<f64>> {
|
||||
let Some(ctl) = ctl_for_name(&name)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
ctl.value()
|
||||
.map_err(map_sysctl_error)
|
||||
.and_then(value_to_number)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn read_int_blocking(_: String) -> Result<Option<f64>> {
|
||||
Err(unsupported_platform_error())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read_string_blocking(name: String) -> Result<Option<String>> {
|
||||
let Some(ctl) = ctl_for_name(&name)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
match ctl.value().map_err(map_sysctl_error)? {
|
||||
CtlValue::None => Ok(None),
|
||||
CtlValue::String(value) => Ok(Some(value)),
|
||||
_ => Err(Error::new(
|
||||
Status::GenericFailure,
|
||||
"sysctlbyname failed (errno 22)".to_owned(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn read_string_blocking(_: String) -> Result<Option<String>> {
|
||||
Err(unsupported_platform_error())
|
||||
}
|
||||
|
||||
async fn spawn_sysctl<T: Send + 'static>(
|
||||
work: impl FnOnce() -> Result<T> + Send + 'static,
|
||||
) -> Result<T> {
|
||||
tokio::task::spawn_blocking(work).await.map_err(|error| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("sysctl worker failed: {error}"),
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
#[napi(js_name = "sysctlByNameInt")]
|
||||
pub async fn sysctl_by_name_int(name: String) -> Result<Option<f64>> {
|
||||
if name.is_empty() {
|
||||
return Err(invalid_name_error("sysctlByNameInt"));
|
||||
}
|
||||
spawn_sysctl(move || read_int_blocking(name)).await
|
||||
}
|
||||
|
||||
#[napi(js_name = "sysctlByNameString")]
|
||||
pub async fn sysctl_by_name_string(name: String) -> Result<Option<String>> {
|
||||
if name.is_empty() {
|
||||
return Err(invalid_name_error("sysctlByNameString"));
|
||||
}
|
||||
spawn_sysctl(move || read_string_blocking(name)).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_name_error_mentions_function_name() {
|
||||
let error = invalid_name_error("sysctlByNameInt");
|
||||
assert_eq!(Status::InvalidArg, error.status);
|
||||
assert!(error.reason.contains("sysctlByNameInt"));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn non_macos_returns_same_synthetic_errno_as_previous_js_contract() {
|
||||
let error = unsupported_platform_error();
|
||||
assert_eq!("sysctlbyname failed (errno 22)", error.reason);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn missing_sysctl_maps_to_null() {
|
||||
assert!(
|
||||
ctl_for_name("fluxer.definitely_missing_sysctl")
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user