mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 00:28:33 +00:00
fix(native): potential sharablecontent icon/name crash (#10464)
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -69,6 +69,7 @@ dependencies = [
|
||||
"core-foundation",
|
||||
"coreaudio-rs",
|
||||
"dispatch2",
|
||||
"libc",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
|
||||
@@ -24,6 +24,7 @@ dispatch2 = "0.2"
|
||||
dotenvy = "0.15"
|
||||
file-format = { version = "0.26", features = ["reader"] }
|
||||
homedir = "0.3"
|
||||
libc = "0.2"
|
||||
mimalloc = "0.1"
|
||||
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
import { exec } from 'node:child_process';
|
||||
import { createServer } from 'node:http';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import {
|
||||
type Application,
|
||||
@@ -58,7 +56,7 @@ interface RecordingMetadata {
|
||||
interface AppInfo {
|
||||
app: Application;
|
||||
processId: number;
|
||||
processGroupId: number | null;
|
||||
processGroupId: number;
|
||||
bundleIdentifier: string;
|
||||
name: string;
|
||||
running: boolean;
|
||||
@@ -226,8 +224,7 @@ async function startRecording(app: Application) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the root app of the process group
|
||||
const processGroupId = await getProcessGroupId(app.processId);
|
||||
const processGroupId = app.processGroupId;
|
||||
const rootApp = processGroupId
|
||||
? (shareableContent
|
||||
.applications()
|
||||
@@ -431,42 +428,25 @@ async function setupRecordingsWatcher() {
|
||||
}
|
||||
}
|
||||
|
||||
// Process management
|
||||
async function getProcessGroupId(pid: number): Promise<number | null> {
|
||||
try {
|
||||
const execAsync = promisify(exec);
|
||||
const { stdout } = await execAsync(`ps -o pgid -p ${pid}`);
|
||||
const lines = stdout.trim().split('\n');
|
||||
if (lines.length < 2) return null;
|
||||
|
||||
const pgid = parseInt(lines[1].trim(), 10);
|
||||
return isNaN(pgid) ? null : pgid;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Application management
|
||||
const shareableContent = new ShareableContent();
|
||||
|
||||
async function getAllApps(): Promise<AppInfo[]> {
|
||||
const apps = await Promise.all(
|
||||
shareableContent.applications().map(async app => {
|
||||
try {
|
||||
return {
|
||||
app,
|
||||
processId: app.processId,
|
||||
processGroupId: await getProcessGroupId(app.processId),
|
||||
bundleIdentifier: app.bundleIdentifier,
|
||||
name: app.name,
|
||||
running: app.isRunning,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
const apps = shareableContent.applications().map(app => {
|
||||
try {
|
||||
return {
|
||||
app,
|
||||
processId: app.processId,
|
||||
processGroupId: app.processGroupId,
|
||||
bundleIdentifier: app.bundleIdentifier,
|
||||
name: app.name,
|
||||
running: app.isRunning,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const filteredApps = apps.filter(
|
||||
(v): v is AppInfo =>
|
||||
|
||||
1
packages/frontend/native/index.d.ts
vendored
1
packages/frontend/native/index.d.ts
vendored
@@ -3,6 +3,7 @@
|
||||
export declare class Application {
|
||||
static tapGlobalAudio(excludedProcesses: Array<Application> | undefined | null, audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioTapStream
|
||||
get processId(): number
|
||||
get processGroupId(): number
|
||||
get bundleIdentifier(): string
|
||||
get name(): string
|
||||
get icon(): Buffer
|
||||
|
||||
@@ -16,6 +16,7 @@ block2 = { workspace = true }
|
||||
core-foundation = { workspace = true, features = ["with-uuid"] }
|
||||
coreaudio-rs = { workspace = true }
|
||||
dispatch2 = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
objc2 = { workspace = true }
|
||||
objc2-foundation = { workspace = true }
|
||||
screencapturekit = { workspace = true }
|
||||
|
||||
@@ -20,6 +20,7 @@ use coreaudio::sys::{
|
||||
AudioObjectAddPropertyListenerBlock, AudioObjectID, AudioObjectPropertyAddress,
|
||||
AudioObjectRemovePropertyListenerBlock,
|
||||
};
|
||||
use libc;
|
||||
use napi::{
|
||||
bindgen_prelude::{Buffer, Error, Float32Array, Result, Status},
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
@@ -27,7 +28,6 @@ use napi::{
|
||||
use napi_derive::napi;
|
||||
use objc2::{
|
||||
msg_send,
|
||||
rc::Retained,
|
||||
runtime::{AnyClass, AnyObject},
|
||||
Encode, Encoding,
|
||||
};
|
||||
@@ -114,136 +114,227 @@ impl TappableApplication {
|
||||
}
|
||||
|
||||
fn name(&self) -> Result<String> {
|
||||
let pid = self.process_id()?;
|
||||
// Use catch_unwind to prevent any panics
|
||||
let name_result = std::panic::catch_unwind(|| {
|
||||
// Get process ID with error handling
|
||||
let pid = match self.process_id() {
|
||||
Ok(pid) => pid,
|
||||
Err(_) => {
|
||||
return Ok(String::new());
|
||||
}
|
||||
};
|
||||
|
||||
// Get NSRunningApplication class
|
||||
let running_app_class = NSRUNNING_APPLICATION_CLASS.as_ref().ok_or_else(|| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
"NSRunningApplication class not found",
|
||||
)
|
||||
})?;
|
||||
// Get NSRunningApplication class with error handling
|
||||
let running_app_class = match NSRUNNING_APPLICATION_CLASS.as_ref() {
|
||||
Some(class) => class,
|
||||
None => {
|
||||
return Ok(String::new());
|
||||
}
|
||||
};
|
||||
|
||||
// Get running application with PID
|
||||
let running_app: *mut AnyObject =
|
||||
unsafe { msg_send![*running_app_class, runningApplicationWithProcessIdentifier: pid] };
|
||||
if running_app.is_null() {
|
||||
return Ok(String::new());
|
||||
// Get running application with PID
|
||||
let running_app: *mut AnyObject =
|
||||
unsafe { msg_send![*running_app_class, runningApplicationWithProcessIdentifier: pid] };
|
||||
|
||||
if running_app.is_null() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
// Instead of using Retained::from_raw which takes ownership,
|
||||
// we'll just copy the string value and let the Objective-C runtime
|
||||
// handle the memory management of the original object
|
||||
unsafe {
|
||||
// Get localized name
|
||||
let name_ptr: *mut NSString = msg_send![running_app, localizedName];
|
||||
if name_ptr.is_null() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
// Create a copy of the string without taking ownership of the NSString
|
||||
let length: usize = msg_send![name_ptr, length];
|
||||
let utf8_ptr: *const u8 = msg_send![name_ptr, UTF8String];
|
||||
|
||||
if utf8_ptr.is_null() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let bytes = std::slice::from_raw_parts(utf8_ptr, length);
|
||||
match std::str::from_utf8(bytes) {
|
||||
Ok(s) => Ok(s.to_string()),
|
||||
Err(_) => Ok(String::new()),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle any panics that might have occurred
|
||||
match name_result {
|
||||
Ok(result) => result,
|
||||
Err(_) => Ok(String::new()),
|
||||
}
|
||||
|
||||
// Get localized name
|
||||
let name: *mut NSString = unsafe { msg_send![running_app, localizedName] };
|
||||
if name.is_null() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
// Create a safe wrapper and convert to string
|
||||
let name = unsafe {
|
||||
Retained::from_raw(name).ok_or_else(|| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
"Failed to create safe wrapper for localizedName",
|
||||
)
|
||||
})?
|
||||
};
|
||||
Ok(name.to_string())
|
||||
}
|
||||
|
||||
fn icon(&self) -> Result<Vec<u8>> {
|
||||
let pid = self.process_id()?;
|
||||
|
||||
// Get NSRunningApplication class
|
||||
let running_app_class = NSRUNNING_APPLICATION_CLASS.as_ref().ok_or_else(|| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
"NSRunningApplication class not found",
|
||||
)
|
||||
})?;
|
||||
|
||||
// Get running application with PID
|
||||
let running_app: *mut AnyObject =
|
||||
unsafe { msg_send![*running_app_class, runningApplicationWithProcessIdentifier: pid] };
|
||||
if running_app.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Get original icon
|
||||
let icon: *mut AnyObject = msg_send![running_app, icon];
|
||||
if icon.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Create a new NSImage with 64x64 size
|
||||
let nsimage_class = AnyClass::get(c"NSImage")
|
||||
.ok_or_else(|| Error::new(Status::GenericFailure, "NSImage class not found"))?;
|
||||
let resized_image: *mut AnyObject = msg_send![nsimage_class, alloc];
|
||||
let resized_image: *mut AnyObject =
|
||||
msg_send![resized_image, initWithSize: NSSize { width: 64.0, height: 64.0 }];
|
||||
let _: () = msg_send![resized_image, lockFocus];
|
||||
|
||||
// Define drawing rectangle for 64x64 image
|
||||
let draw_rect = NSRect {
|
||||
origin: NSPoint { x: 0.0, y: 0.0 },
|
||||
size: NSSize {
|
||||
width: 64.0,
|
||||
height: 64.0,
|
||||
},
|
||||
// Use catch_unwind to prevent any panics
|
||||
let icon_result = std::panic::catch_unwind(|| {
|
||||
// Get process ID with error handling
|
||||
let pid = match self.process_id() {
|
||||
Ok(pid) => pid,
|
||||
Err(_) => {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
};
|
||||
|
||||
// Draw the original icon into draw_rect (using NSCompositingOperationCopy = 2)
|
||||
let _: () = msg_send![icon, drawInRect: draw_rect, fromRect: NSRect { origin: NSPoint { x: 0.0, y: 0.0 }, size: NSSize { width: 0.0, height: 0.0 } }, operation: 2, fraction: 1.0];
|
||||
let _: () = msg_send![resized_image, unlockFocus];
|
||||
// Get NSRunningApplication class with error handling
|
||||
let running_app_class = match NSRUNNING_APPLICATION_CLASS.as_ref() {
|
||||
Some(class) => class,
|
||||
None => {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
};
|
||||
|
||||
// Get TIFF representation from the downsized image
|
||||
let tiff_data: *mut AnyObject = msg_send![resized_image, TIFFRepresentation];
|
||||
if tiff_data.is_null() {
|
||||
// Get running application with PID
|
||||
let running_app: *mut AnyObject =
|
||||
unsafe { msg_send![*running_app_class, runningApplicationWithProcessIdentifier: pid] };
|
||||
if running_app.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Create bitmap image rep from TIFF
|
||||
let bitmap_class = AnyClass::get(c"NSBitmapImageRep")
|
||||
.ok_or_else(|| Error::new(Status::GenericFailure, "NSBitmapImageRep class not found"))?;
|
||||
let bitmap: *mut AnyObject = msg_send![bitmap_class, imageRepWithData: tiff_data];
|
||||
if bitmap.is_null() {
|
||||
return Ok(Vec::new());
|
||||
unsafe {
|
||||
// Get original icon
|
||||
let icon: *mut AnyObject = msg_send![running_app, icon];
|
||||
if icon.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Create a new NSImage with 64x64 size
|
||||
let nsimage_class = match AnyClass::get(c"NSImage") {
|
||||
Some(class) => class,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
let resized_image: *mut AnyObject = msg_send![nsimage_class, alloc];
|
||||
if resized_image.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let resized_image: *mut AnyObject =
|
||||
msg_send![resized_image, initWithSize: NSSize { width: 64.0, height: 64.0 }];
|
||||
if resized_image.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let _: () = msg_send![resized_image, lockFocus];
|
||||
|
||||
// Define drawing rectangle for 64x64 image
|
||||
let draw_rect = NSRect {
|
||||
origin: NSPoint { x: 0.0, y: 0.0 },
|
||||
size: NSSize {
|
||||
width: 64.0,
|
||||
height: 64.0,
|
||||
},
|
||||
};
|
||||
|
||||
// Draw the original icon into draw_rect (using NSCompositingOperationCopy = 2)
|
||||
let _: () = msg_send![icon, drawInRect: draw_rect, fromRect: NSRect { origin: NSPoint { x: 0.0, y: 0.0 }, size: NSSize { width: 0.0, height: 0.0 } }, operation: 2, fraction: 1.0];
|
||||
let _: () = msg_send![resized_image, unlockFocus];
|
||||
|
||||
// Get TIFF representation from the downsized image
|
||||
let tiff_data: *mut AnyObject = msg_send![resized_image, TIFFRepresentation];
|
||||
if tiff_data.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Create bitmap image rep from TIFF
|
||||
let bitmap_class = match AnyClass::get(c"NSBitmapImageRep") {
|
||||
Some(class) => class,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
let bitmap: *mut AnyObject = msg_send![bitmap_class, imageRepWithData: tiff_data];
|
||||
if bitmap.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Create properties dictionary with compression factor
|
||||
let dict_class = match AnyClass::get(c"NSMutableDictionary") {
|
||||
Some(class) => class,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
let properties: *mut AnyObject = msg_send![dict_class, dictionary];
|
||||
if properties.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Add compression properties
|
||||
let compression_key = NSString::from_str("NSImageCompressionFactor");
|
||||
let number_class = match AnyClass::get(c"NSNumber") {
|
||||
Some(class) => class,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
let compression_value: *mut AnyObject = msg_send![number_class, numberWithDouble: 0.8];
|
||||
if compression_value.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let _: () = msg_send![properties, setObject: compression_value, forKey: &*compression_key];
|
||||
|
||||
// Get PNG data with properties
|
||||
let png_data: *mut AnyObject =
|
||||
msg_send![bitmap, representationUsingType: 4, properties: properties]; // 4 = PNG
|
||||
|
||||
if png_data.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Get bytes from NSData
|
||||
let bytes: *const u8 = msg_send![png_data, bytes];
|
||||
let length: usize = msg_send![png_data, length];
|
||||
|
||||
if bytes.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Copy bytes into a Vec<u8> instead of using the original memory
|
||||
let data = std::slice::from_raw_parts(bytes, length).to_vec();
|
||||
Ok(data)
|
||||
}
|
||||
});
|
||||
|
||||
// Handle any panics that might have occurred
|
||||
match icon_result {
|
||||
Ok(result) => result,
|
||||
Err(_) => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_group_id(&self) -> Result<i32> {
|
||||
// Use catch_unwind to prevent any panics
|
||||
let pgid_result = std::panic::catch_unwind(|| {
|
||||
// First get the process ID
|
||||
let pid = match self.process_id() {
|
||||
Ok(pid) => pid,
|
||||
Err(_) => {
|
||||
return Ok(-1); // Return -1 for error cases
|
||||
}
|
||||
};
|
||||
|
||||
// Call libc's getpgid function to get the process group ID
|
||||
let pgid = unsafe { libc::getpgid(pid) };
|
||||
|
||||
// getpgid returns -1 on error
|
||||
if pgid == -1 {
|
||||
return Ok(-1);
|
||||
}
|
||||
|
||||
// Create properties dictionary with compression factor
|
||||
let dict_class = AnyClass::get(c"NSMutableDictionary").ok_or_else(|| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
"NSMutableDictionary class not found",
|
||||
)
|
||||
})?;
|
||||
let properties: *mut AnyObject = msg_send![dict_class, dictionary];
|
||||
Ok(pgid)
|
||||
});
|
||||
|
||||
// Add compression properties
|
||||
let compression_key = NSString::from_str("NSImageCompressionFactor");
|
||||
let number_class = AnyClass::get(c"NSNumber")
|
||||
.ok_or_else(|| Error::new(Status::GenericFailure, "NSNumber class not found"))?;
|
||||
let compression_value: *mut AnyObject = msg_send![number_class, numberWithDouble: 0.8];
|
||||
let _: () = msg_send![properties, setObject: compression_value, forKey: &*compression_key];
|
||||
|
||||
// Get PNG data with properties
|
||||
let png_data: *mut AnyObject =
|
||||
msg_send![bitmap, representationUsingType: 4, properties: properties]; // 4 = PNG
|
||||
|
||||
if png_data.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Get bytes from NSData
|
||||
let bytes: *const u8 = msg_send![png_data, bytes];
|
||||
let length: usize = msg_send![png_data, length];
|
||||
|
||||
if bytes.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Copy bytes into a Vec<u8>
|
||||
let data = std::slice::from_raw_parts(bytes, length).to_vec();
|
||||
Ok(data)
|
||||
// Handle any panics
|
||||
match pgid_result {
|
||||
Ok(result) => result,
|
||||
Err(_) => Ok(-1),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,6 +344,7 @@ pub struct Application {
|
||||
inner: TappableApplication,
|
||||
pub(crate) object_id: AudioObjectID,
|
||||
pub(crate) process_id: i32,
|
||||
pub(crate) process_group_id: i32,
|
||||
pub(crate) bundle_identifier: String,
|
||||
pub(crate) name: String,
|
||||
}
|
||||
@@ -264,11 +356,13 @@ impl Application {
|
||||
let bundle_identifier = app.bundle_identifier()?;
|
||||
let name = app.name()?;
|
||||
let process_id = app.process_id()?;
|
||||
let process_group_id = app.process_group_id()?;
|
||||
|
||||
Ok(Self {
|
||||
inner: app,
|
||||
object_id,
|
||||
process_id,
|
||||
process_group_id,
|
||||
bundle_identifier,
|
||||
name,
|
||||
})
|
||||
@@ -294,6 +388,11 @@ impl Application {
|
||||
self.process_id
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn process_group_id(&self) -> i32 {
|
||||
self.process_group_id
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn bundle_identifier(&self) -> String {
|
||||
self.bundle_identifier.clone()
|
||||
@@ -306,16 +405,40 @@ impl Application {
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn icon(&self) -> Result<Buffer> {
|
||||
let icon = self.inner.icon()?;
|
||||
Ok(Buffer::from(icon))
|
||||
// Use catch_unwind to prevent any panics
|
||||
let result = std::panic::catch_unwind(|| match self.inner.icon() {
|
||||
Ok(icon) => Ok(Buffer::from(icon)),
|
||||
Err(_) => Ok(Buffer::from(Vec::<u8>::new())),
|
||||
});
|
||||
|
||||
// Handle any panics
|
||||
match result {
|
||||
Ok(result) => result,
|
||||
Err(_) => Ok(Buffer::from(Vec::<u8>::new())),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn get_is_running(&self) -> Result<bool> {
|
||||
Ok(get_process_property(
|
||||
&self.object_id,
|
||||
kAudioProcessPropertyIsRunningInput,
|
||||
)?)
|
||||
// Use catch_unwind to prevent any panics
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
match get_process_property(&self.object_id, kAudioProcessPropertyIsRunningInput) {
|
||||
Ok(is_running) => Ok(is_running),
|
||||
Err(_) => {
|
||||
// Default to true to avoid potential issues
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle any panics
|
||||
match result {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
// Default to true to avoid potential issues
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
||||
Reference in New Issue
Block a user