Skip to content

Commit c70f946

Browse files
committed
get process icons on macOS (very WIP)
1 parent 362f1c3 commit c70f946

7 files changed

Lines changed: 206 additions & 37 deletions

File tree

Cargo.lock

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ clap = { version = "4.5.41", features = ["derive"] }
5858
tokio = { version = "1.46.1", features = ["macros"] }
5959
async-channel = "2.5.0"
6060
listeners = { git = "https://github.com/islameehassan/listeners" }
61+
cocoa = "0.24"
62+
objc = "0.2"
63+
libc = "0.2.174"
64+
image = "0.24"
6165

6266
[target.'cfg(windows)'.dependencies]
6367
gag = "1.0.0"

src/gui/pages/overview_page.rs

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ use iced::widget::scrollable::Direction;
4747
use iced::widget::text::LineHeight;
4848
use iced::widget::tooltip::Position;
4949
use iced::widget::{
50-
Button, Column, Container, Row, Rule, Scrollable, Space, Text, Tooltip, button,
50+
Button, Column, Container, Image, Row, Rule, Scrollable, Space, Text, Tooltip, button,
5151
horizontal_space, vertical_space,
5252
};
5353
use iced::{Alignment, Font, Length, Padding};
@@ -98,6 +98,8 @@ pub fn overview_page(sniffer: &Sniffer) -> Container<Message, StyleType> {
9898

9999
let container_info = col_info(sniffer);
100100

101+
let col_service = col_service(sniffer);
102+
101103
let container_report = row_report(sniffer);
102104

103105
body = body
@@ -110,7 +112,14 @@ pub fn overview_page(sniffer: &Sniffer) -> Container<Message, StyleType> {
110112
.height(280)
111113
.spacing(10)
112114
.push(container_info)
113-
.push(container_chart),
115+
.push(container_chart)
116+
.push(
117+
Container::new(col_service)
118+
.width(350)
119+
.height(Length::Fill)
120+
.padding(Padding::new(10.0).top(0).bottom(5))
121+
.class(ContainerType::BorderedRound),
122+
),
114123
)
115124
.push(container_report);
116125
}
@@ -222,28 +231,20 @@ fn body_pcap_error<'a>(
222231

223232
fn row_report<'a>(sniffer: &Sniffer) -> Row<'a, Message, StyleType> {
224233
let col_host = col_host(sniffer);
225-
let col_service = col_service(sniffer);
226234
let col_process = col_process(sniffer);
227235

228236
Row::new()
229237
.spacing(10)
230238
.push(
231239
Container::new(col_host)
232-
.width(Length::FillPortion(5))
233-
.height(Length::Fill)
234-
.padding(Padding::new(10.0).top(0).bottom(5))
235-
.class(ContainerType::BorderedRound),
236-
)
237-
.push(
238-
Container::new(col_service)
239-
.width(Length::FillPortion(2))
240+
.width(Length::FillPortion(6))
240241
.height(Length::Fill)
241242
.padding(Padding::new(10.0).top(0).bottom(5))
242243
.class(ContainerType::BorderedRound),
243244
)
244245
.push(
245246
Container::new(col_process)
246-
.width(Length::FillPortion(2))
247+
.width(Length::FillPortion(4))
247248
.height(Length::Fill)
248249
.padding(Padding::new(10.0).top(0).bottom(5))
249250
.class(ContainerType::BorderedRound),
@@ -410,11 +411,11 @@ fn col_process<'a>(sniffer: &Sniffer) -> Column<'a, Message, StyleType> {
410411
let entries = get_process_entries(&sniffer.info_traffic, chart_type, sniffer.process_sort_type);
411412
let first_entry_data_info = entries
412413
.iter()
413-
.map(|&(_, d)| d)
414-
.max_by(|d1, d2| d1.compare(d2, SortType::Ascending, chart_type))
415-
.unwrap_or_default();
414+
.map(|&(_, (d, _))| d)
415+
.max_by(|d1, d2| d1.compare(&d2, SortType::Ascending, chart_type))
416+
.unwrap();
416417

417-
for (process, data_info) in &entries {
418+
for (process, (data_info, handle)) in &entries {
418419
let content = simpler_bar(
419420
process.to_string(),
420421
data_info,
@@ -424,12 +425,22 @@ fn col_process<'a>(sniffer: &Sniffer) -> Column<'a, Message, StyleType> {
424425
);
425426

426427
scroll_process = scroll_process.push(
427-
button(content)
428-
.padding(Padding::new(5.0).right(15).left(10))
429-
.on_press(Message::Search(SearchParameters::new_process_search(
430-
process,
431-
)))
432-
.class(ButtonType::Neutral),
428+
button(
429+
Row::new()
430+
.spacing(5)
431+
.align_y(Vertical::Center)
432+
.push_maybe(
433+
handle
434+
.clone()
435+
.map(|h| Image::new(h).height(FLAGS_HEIGHT_BIG)),
436+
)
437+
.push(content),
438+
)
439+
.padding(Padding::new(5.0).right(15).left(10))
440+
.on_press(Message::Search(SearchParameters::new_process_search(
441+
process,
442+
)))
443+
.class(ButtonType::Neutral),
433444
);
434445
}
435446

@@ -588,7 +599,7 @@ fn col_info<'a>(sniffer: &Sniffer) -> Container<'a, Message, StyleType> {
588599
.push(donut_row.height(Length::Fill));
589600

590601
Container::new(content)
591-
.width(400)
602+
.width(350)
592603
.padding(Padding::new(5.0).top(10))
593604
.align_x(Alignment::Center)
594605
.class(ContainerType::BorderedRound)

src/networking/manage_packets.rs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::HashMap;
2+
use std::ffi::{CStr, c_int};
23
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
34

45
use etherparse::{EtherType, LaxPacketHeaders, LinkHeader, NetHeaders, TransportHeader};
@@ -17,6 +18,12 @@ use crate::networking::types::service_query::ServiceQuery;
1718
use crate::networking::types::traffic_direction::TrafficDirection;
1819
use crate::networking::types::traffic_type::TrafficType;
1920
use crate::{IpVersion, Protocol};
21+
use cocoa::base::{id, nil};
22+
use cocoa::foundation::{NSSize, NSString};
23+
use iced::widget::Image;
24+
use iced::widget::image::Handle;
25+
use image::{GenericImageView, RgbaImage};
26+
use objc::{class, msg_send, sel, sel_impl};
2027
use std::fmt::Write;
2128

2229
include!(concat!(env!("OUT_DIR"), "/services.rs"));
@@ -256,10 +263,11 @@ pub fn modify_or_insert_in_map(
256263
icmp_type: IcmpType,
257264
arp_type: ArpType,
258265
exchanged_bytes: u128,
259-
) -> (TrafficDirection, Service, String) {
266+
) -> (TrafficDirection, Service, String, Option<Handle>) {
260267
let mut traffic_direction = TrafficDirection::default();
261268
let mut service = Service::Unknown;
262269
let mut process = "-".to_string();
270+
let mut picon = None;
263271

264272
if !info_traffic_msg.map.contains_key(key) {
265273
// first occurrence of key (in this time interval)
@@ -284,7 +292,38 @@ pub fn modify_or_insert_in_map(
284292
.into_iter()
285293
.flatten()
286294
.next()
287-
.map_or("?".to_string(), |p| p.name);
295+
.map_or("?".to_string(), |p| {
296+
let path = get_executable_path(p.pid as i32);
297+
let mut image: Option<iced::widget::Image> = None;
298+
println!("Process: {} (PID: {})", p.name, p.pid);
299+
if let Some(app_path) = path {
300+
println!("--> Exe path: {}", app_path);
301+
if let Some(bundle_path) = find_app_bundle_path(&app_path) {
302+
println!("--> Bundle path: {}", bundle_path);
303+
if let Some(tiff) = get_icon_tiff_bytes(&bundle_path) {
304+
let img =
305+
image::load_from_memory(&tiff).expect("Failed to decode image");
306+
// resize the image
307+
let resized =
308+
img.resize_exact(64, 64, image::imageops::FilterType::Lanczos3);
309+
let (width, height) = resized.dimensions();
310+
println!("--> Icon size: {}x{}", width, height);
311+
// crop the image
312+
let sub =
313+
image::imageops::crop_imm(&resized, 6, 6, 52, 52).to_image();
314+
let (width, height) = sub.dimensions();
315+
println!("--> Cropped icon size: {}x{}", width, height);
316+
317+
picon = Some(iced::widget::image::Handle::from_rgba(
318+
width,
319+
height,
320+
sub.into_raw(),
321+
));
322+
}
323+
}
324+
}
325+
p.name
326+
});
288327
}
289328
}
290329

@@ -335,9 +374,70 @@ pub fn modify_or_insert_in_map(
335374
new_info.traffic_direction,
336375
new_info.service,
337376
new_info.process.clone(),
377+
picon,
338378
)
339379
}
340380

381+
unsafe extern "C" {
382+
fn proc_pidpath(pid: c_int, buffer: *mut libc::c_void, buffersize: u32) -> c_int;
383+
}
384+
385+
fn get_executable_path(pid: i32) -> Option<String> {
386+
let mut buf = vec![0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
387+
let ret = unsafe { proc_pidpath(pid, buf.as_mut_ptr() as *mut _, buf.len() as u32) };
388+
389+
if ret > 0 {
390+
let path = CStr::from_bytes_until_nul(&buf).unwrap();
391+
Some(path.to_string_lossy().into_owned())
392+
} else {
393+
None
394+
}
395+
}
396+
397+
fn find_app_bundle_path(exe_path: &str) -> Option<String> {
398+
let mut current = std::path::Path::new(exe_path);
399+
let mut last_app_dir: Option<std::path::PathBuf> = None;
400+
401+
// Walk up the path, including the input
402+
loop {
403+
if let Some(file_name) = current.file_name() {
404+
if file_name.to_string_lossy().ends_with(".app") {
405+
last_app_dir = Some(current.to_path_buf());
406+
}
407+
}
408+
409+
match current.parent() {
410+
Some(parent) => current = parent,
411+
None => break,
412+
}
413+
}
414+
415+
last_app_dir.map(|p| p.to_string_lossy().into_owned())
416+
}
417+
418+
fn get_icon_tiff_bytes(app_path: &str) -> Option<Vec<u8>> {
419+
unsafe {
420+
let ns_app_path: id = NSString::alloc(nil).init_str(app_path);
421+
let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
422+
let icon: id = msg_send![workspace, iconForFile: ns_app_path];
423+
424+
if icon == nil {
425+
return None;
426+
}
427+
428+
// Get TIFF representation (NSData)
429+
let tiff_data: id = msg_send![icon, TIFFRepresentation];
430+
if tiff_data == nil {
431+
return None;
432+
}
433+
434+
// Convert NSData to Vec<u8>
435+
let length: usize = msg_send![tiff_data, length];
436+
let bytes: *const u8 = msg_send![tiff_data, bytes];
437+
Some(std::slice::from_raw_parts(bytes, length).to_vec())
438+
}
439+
}
440+
341441
/// Returns the traffic direction observed (incoming or outgoing)
342442
fn get_traffic_direction(
343443
source_ip: &IpAddr,

src/networking/parse_packets.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ pub fn parse_packets(
144144
file.write(&packet);
145145
}
146146
// update the map
147-
let (traffic_direction, service, process) = modify_or_insert_in_map(
147+
let (traffic_direction, service, process, picon) = modify_or_insert_in_map(
148148
&mut info_traffic_msg,
149149
&key,
150150
&cs,
@@ -280,10 +280,16 @@ pub fn parse_packets(
280280
.processes
281281
.entry(process)
282282
.and_modify(|data_info| {
283-
data_info.add_packet(exchanged_bytes, traffic_direction);
283+
data_info.0.add_packet(exchanged_bytes, traffic_direction);
284284
})
285285
.or_insert_with(|| {
286-
DataInfo::new_with_first_packet(exchanged_bytes, traffic_direction)
286+
(
287+
DataInfo::new_with_first_packet(
288+
exchanged_bytes,
289+
traffic_direction,
290+
),
291+
picon,
292+
)
287293
});
288294
}
289295

0 commit comments

Comments
 (0)