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.
184 lines
5.4 KiB
Rust
184 lines
5.4 KiB
Rust
// 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()
|
|
);
|
|
}
|
|
}
|