Per-Process Thread, Socket & File Descriptor Inspection #12

Closed
opened 2026-09-08 10:28:49 +01:00 by gronod · 1 comment
Owner

Resolved in Milestone 3. Implemented MMProcessDetailInspector.h/.m inspecting per-process thread states, CPU usage, open vnode file descriptors, and active sockets with TCP states. Verified with unit tests and integrated into Process Detail sheet. Merged into develop.

Resolved in Milestone 3. Implemented `MMProcessDetailInspector.h/.m` inspecting per-process thread states, CPU usage, open vnode file descriptors, and active sockets with TCP states. Verified with unit tests and integrated into Process Detail sheet. Merged into `develop`.
Author
Owner

Technical Implementation Plan & Code Solution

1. Process Drill-Down Architecture

To inspect individual threads, open files, and active sockets of a target process on demand:

  • Threads: Enumerated via proc_pidinfo(..., PROC_PIDLISTTHREADS) and queried via PROC_PIDTHREADINFO.
  • File Descriptors: Enumerated via proc_pidinfo(..., PROC_PIDLISTFDS) and resolved to vnodes via PROC_PIDFDVNODEPATHINFO.
  • Sockets: Resolved via PROC_PIDFDSOCKETINFO.

2. Objective-C Inspector (MMProcessInspector.m)

#import "MMProcessInspector.h"
#import <libproc.h>
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>

@implementation MMProcessInspector

+ (NSArray<NSDictionary *> *)inspectThreadsForPID:(pid_t)pid {
    int byteSize = proc_pidinfo(pid, PROC_PIDLISTTHREADS, 0, NULL, 0);
    if (byteSize <= 0) return @[];

    int count = byteSize / sizeof(uint64_t);
    uint64_t *threadIds = malloc(byteSize);
    if (!threadIds) return @[];

    proc_pidinfo(pid, PROC_PIDLISTTHREADS, 0, threadIds, byteSize);
    NSMutableArray *threads = [NSMutableArray arrayWithCapacity:count];

    for (int i = 0; i < count; i++) {
        struct proc_threadinfo thInfo;
        int ret = proc_pidinfo(pid, PROC_PIDTHREADINFO, threadIds[i], &thInfo, sizeof(thInfo));
        if (ret == sizeof(thInfo)) {
            [threads addObject:@{
                @"threadId": @(threadIds[i]),
                @"userCpuTimeNs": @(thInfo.pth_user_time),
                @"systemCpuTimeNs": @(thInfo.pth_system_time),
                @"state": @(thInfo.pth_run_state),
                @"flags": @(thInfo.pth_flags),
                @"priority": @(thInfo.pth_priority)
            }];
        }
    }
    free(threadIds);
    return [threads copy];
}

+ (NSArray<NSDictionary *> *)inspectOpenFilesForPID:(pid_t)pid {
    int byteSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0);
    if (byteSize <= 0) return @[];

    int count = byteSize / sizeof(struct proc_fdinfo);
    struct proc_fdinfo *fds = malloc(byteSize);
    if (!fds) return @[];

    proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fds, byteSize);
    NSMutableArray *files = [NSMutableArray array];

    for (int i = 0; i < count; i++) {
        if (fds[i].proc_fdtype == PROX_FDTYPE_VNODE) {
            struct vnode_fdinfowithpath pathInfo;
            int ret = proc_pidfdinfo(pid, fds[i].proc_fd, PROC_PIDFDVNODEPATHINFO, &pathInfo, sizeof(pathInfo));
            if (ret == sizeof(pathInfo)) {
                NSString *path = [NSString stringWithUTF8String:pathInfo.pvip.vip_path];
                [files addObject:@{
                    @"fd": @(fds[i].proc_fd),
                    @"type": @"file",
                    @"path": path ?: @""
                }];
            }
        }
    }
    free(fds);
    return [files copy];
}
@end

3. Swift UI Integration

public struct ProcessThreadDetail: Identifiable, Sendable {
    public var id: UInt64 { threadId }
    public let threadId: UInt64
    public let userCpuTimeNs: UInt64
    public let systemCpuTimeNs: UInt64
    public let state: Int
}

4. Permission Boundary

If the target process belongs to root and MacMonitor is running under standard user credentials, proc_pidfdinfo will return EPERM cleanly without crashing. The inspector catches and displays a "Permission Denied (Protected Process)" state.

## Technical Implementation Plan & Code Solution ### 1. Process Drill-Down Architecture To inspect individual threads, open files, and active sockets of a target process on demand: - **Threads**: Enumerated via `proc_pidinfo(..., PROC_PIDLISTTHREADS)` and queried via `PROC_PIDTHREADINFO`. - **File Descriptors**: Enumerated via `proc_pidinfo(..., PROC_PIDLISTFDS)` and resolved to vnodes via `PROC_PIDFDVNODEPATHINFO`. - **Sockets**: Resolved via `PROC_PIDFDSOCKETINFO`. --- ### 2. Objective-C Inspector (`MMProcessInspector.m`) ```objc #import "MMProcessInspector.h" #import <libproc.h> #import <sys/socket.h> #import <netinet/in.h> #import <arpa/inet.h> @implementation MMProcessInspector + (NSArray<NSDictionary *> *)inspectThreadsForPID:(pid_t)pid { int byteSize = proc_pidinfo(pid, PROC_PIDLISTTHREADS, 0, NULL, 0); if (byteSize <= 0) return @[]; int count = byteSize / sizeof(uint64_t); uint64_t *threadIds = malloc(byteSize); if (!threadIds) return @[]; proc_pidinfo(pid, PROC_PIDLISTTHREADS, 0, threadIds, byteSize); NSMutableArray *threads = [NSMutableArray arrayWithCapacity:count]; for (int i = 0; i < count; i++) { struct proc_threadinfo thInfo; int ret = proc_pidinfo(pid, PROC_PIDTHREADINFO, threadIds[i], &thInfo, sizeof(thInfo)); if (ret == sizeof(thInfo)) { [threads addObject:@{ @"threadId": @(threadIds[i]), @"userCpuTimeNs": @(thInfo.pth_user_time), @"systemCpuTimeNs": @(thInfo.pth_system_time), @"state": @(thInfo.pth_run_state), @"flags": @(thInfo.pth_flags), @"priority": @(thInfo.pth_priority) }]; } } free(threadIds); return [threads copy]; } + (NSArray<NSDictionary *> *)inspectOpenFilesForPID:(pid_t)pid { int byteSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); if (byteSize <= 0) return @[]; int count = byteSize / sizeof(struct proc_fdinfo); struct proc_fdinfo *fds = malloc(byteSize); if (!fds) return @[]; proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fds, byteSize); NSMutableArray *files = [NSMutableArray array]; for (int i = 0; i < count; i++) { if (fds[i].proc_fdtype == PROX_FDTYPE_VNODE) { struct vnode_fdinfowithpath pathInfo; int ret = proc_pidfdinfo(pid, fds[i].proc_fd, PROC_PIDFDVNODEPATHINFO, &pathInfo, sizeof(pathInfo)); if (ret == sizeof(pathInfo)) { NSString *path = [NSString stringWithUTF8String:pathInfo.pvip.vip_path]; [files addObject:@{ @"fd": @(fds[i].proc_fd), @"type": @"file", @"path": path ?: @"" }]; } } } free(fds); return [files copy]; } @end ``` --- ### 3. Swift UI Integration ```swift public struct ProcessThreadDetail: Identifiable, Sendable { public var id: UInt64 { threadId } public let threadId: UInt64 public let userCpuTimeNs: UInt64 public let systemCpuTimeNs: UInt64 public let state: Int } ``` --- ### 4. Permission Boundary If the target process belongs to `root` and MacMonitor is running under standard user credentials, `proc_pidfdinfo` will return `EPERM` cleanly without crashing. The inspector catches and displays a "Permission Denied (Protected Process)" state.
gronod added this to the M3: Advanced Kernel, Process & Peripheral Telemetry milestone 2026-09-08 10:42:27 +01:00
gronod added a new dependency 2026-09-08 11:37:06 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/MacMonitor#12