Skip to content

Commit bb84387

Browse files
authored
Merge pull request #1 from jkfran/add-macos-support
2 parents b582075 + c82f6d1 commit bb84387

File tree

8 files changed

+239
-78
lines changed

8 files changed

+239
-78
lines changed

.github/workflows/build.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Build
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
8+
jobs:
9+
build:
10+
strategy:
11+
matrix:
12+
target:
13+
- x86_64-unknown-linux-gnu
14+
- i686-unknown-linux-gnu
15+
- aarch64-unknown-linux-gnu
16+
- armv7-unknown-linux-gnueabihf
17+
- arm-unknown-linux-gnueabihf
18+
- powerpc64le-unknown-linux-gnu
19+
- s390x-unknown-linux-gnu
20+
- aarch64-apple-darwin
21+
- x86_64-apple-darwin
22+
runs-on: ${{ (matrix.target == 'aarch64-apple-darwin' || matrix.target == 'x86_64-apple-darwin') && 'macos-latest' || 'ubuntu-latest' }}
23+
steps:
24+
- name: Checkout repository
25+
uses: actions/checkout@v2
26+
27+
- name: Install Rust
28+
uses: actions-rs/toolchain@v1
29+
with:
30+
toolchain: stable
31+
target: ${{ matrix.target }}
32+
override: true
33+
34+
- name: Build target
35+
uses: actions-rs/cargo@v1
36+
with:
37+
use-cross: true
38+
command: build
39+
args: --release --target ${{ matrix.target }}

.github/workflows/release.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ jobs:
2020
- arm-unknown-linux-gnueabihf
2121
- powerpc64le-unknown-linux-gnu
2222
- s390x-unknown-linux-gnu
23-
runs-on: ubuntu-latest
23+
- aarch64-apple-darwin
24+
- x86_64-apple-darwin
25+
runs-on: ${{ (matrix.target == 'aarch64-apple-darwin' || matrix.target == 'x86_64-apple-darwin') && 'macos-latest' || 'ubuntu-latest' }}
2426
steps:
2527
- name: Checkout repository
2628
uses: actions/checkout@v2

Cargo.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "killport"
3-
version = "0.1.0"
3+
version = "0.5.0"
44
authors = ["Francisco Jimenez Cabrera <jkfran@gmail.com>"]
55
edition = "2021"
66
license = "MIT"
@@ -15,10 +15,16 @@ categories = ["command-line-utilities"]
1515
log = "0.4.17"
1616
env_logger = "0.10.0"
1717
clap-verbosity-flag = "2.0.0"
18-
procfs = "0.15.1"
1918
clap = { version = "4.1.8", features = ["derive"] }
2019
nix = "0.26.2"
2120

21+
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
22+
procfs = "0.15.1"
23+
24+
[target.'cfg(target_os = "macos")'.dependencies]
25+
libproc = "0.13.0"
26+
libc = "0.2"
27+
2228
[dev-dependencies]
2329
assert_cmd = "2.0.10"
2430
tempfile = "3.4.0"

snapcraft.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: killport
2-
version: "0.1.0"
2+
version: "0.5.0"
33
summary: A CLI tool to kill processes using specified ports
44
description: |
55
Killport is a command-line utility to find and kill processes listening on specified ports.

src/process.rs renamed to src/linux.rs

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
//! This module provides functions for working with processes and inodes.
2-
//!
3-
//! It exposes a single public function `kill_processes_by_inode` that attempts to
4-
//! kill processes associated with a specified inode.
5-
61
use log::{debug, info, warn};
72
use nix::sys::signal::{kill, Signal};
83
use nix::unistd::Pid;
@@ -11,6 +6,69 @@ use std::io;
116
use std::io::Error;
127
use std::path::Path;
138

9+
/// Attempts to kill processes listening on the specified `port`.
10+
///
11+
/// Returns a `Result` with `true` if any processes were killed, `false` if no
12+
/// processes were found listening on the port, and an `Error` if the operation
13+
/// failed or the platform is unsupported.
14+
///
15+
/// # Arguments
16+
///
17+
/// * `port` - A u16 value representing the port number.
18+
pub fn kill_processes_by_port(port: u16) -> Result<bool, Error> {
19+
let mut killed_any = false;
20+
21+
let target_inodes = find_target_inodes(port);
22+
23+
if !target_inodes.is_empty() {
24+
for target_inode in target_inodes {
25+
killed_any |= kill_processes_by_inode(target_inode)?;
26+
}
27+
}
28+
29+
Ok(killed_any)
30+
}
31+
32+
/// Finds the inodes associated with the specified `port`.
33+
///
34+
/// Returns a `Vec` of inodes for both IPv4 and IPv6 connections.
35+
///
36+
/// # Arguments
37+
///
38+
/// * `port` - A u16 value representing the port number.
39+
#[cfg(target_os = "linux")]
40+
fn find_target_inodes(port: u16) -> Vec<u64> {
41+
let tcp = procfs::net::tcp().unwrap();
42+
let tcp6 = procfs::net::tcp6().unwrap();
43+
let udp = procfs::net::udp().unwrap();
44+
let udp6 = procfs::net::udp6().unwrap();
45+
let mut target_inodes = Vec::new();
46+
47+
target_inodes.extend(
48+
tcp.into_iter()
49+
.filter(|tcp_entry| tcp_entry.local_address.port() == port)
50+
.map(|tcp_entry| tcp_entry.inode),
51+
);
52+
target_inodes.extend(
53+
tcp6.into_iter()
54+
.filter(|tcp_entry| tcp_entry.local_address.port() == port)
55+
.map(|tcp_entry| tcp_entry.inode),
56+
);
57+
58+
target_inodes.extend(
59+
udp.into_iter()
60+
.filter(|udp_entry| udp_entry.local_address.port() == port)
61+
.map(|udp_entry| udp_entry.inode),
62+
);
63+
target_inodes.extend(
64+
udp6.into_iter()
65+
.filter(|udp_entry| udp_entry.local_address.port() == port)
66+
.map(|udp_entry| udp_entry.inode),
67+
);
68+
69+
target_inodes
70+
}
71+
1472
/// Attempts to kill processes associated with the specified `target_inode`.
1573
///
1674
/// Returns a `Result` with `true` if any processes were killed, and an `Error`
@@ -19,7 +77,7 @@ use std::path::Path;
1977
/// # Arguments
2078
///
2179
/// * `target_inode` - A u64 value representing the target inode.
22-
pub fn kill_processes_by_inode(target_inode: u64) -> Result<bool, Error> {
80+
fn kill_processes_by_inode(target_inode: u64) -> Result<bool, Error> {
2381
let processes = procfs::process::all_processes().unwrap();
2482
let mut killed_any = false;
2583

src/macos.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use libproc::libproc::file_info::pidfdinfo;
2+
use libproc::libproc::file_info::{ListFDs, ProcFDType};
3+
use libproc::libproc::net_info::{SocketFDInfo, SocketInfoKind};
4+
use libproc::libproc::proc_pid::{listpidinfo, pidinfo};
5+
use libproc::libproc::task_info::TaskAllInfo;
6+
use libproc::processes::{pids_by_type, ProcFilter};
7+
use log::{debug, info, warn};
8+
use nix::sys::signal::{self, Signal};
9+
use nix::unistd::Pid;
10+
use std::ffi::CStr;
11+
use std::io;
12+
13+
/// Collect information about all processes.
14+
///
15+
/// # Returns
16+
///
17+
/// A vector containing the information of all processes.
18+
fn collect_proc() -> Vec<TaskAllInfo> {
19+
let mut base_procs = Vec::new();
20+
21+
if let Ok(procs) = pids_by_type(ProcFilter::All) {
22+
for p in procs {
23+
if let Ok(task) = pidinfo::<TaskAllInfo>(p as i32, 0) {
24+
base_procs.push(task);
25+
}
26+
}
27+
}
28+
29+
base_procs
30+
}
31+
32+
/// Kill processes listening on the specified port.
33+
///
34+
/// # Arguments
35+
///
36+
/// * `port` - The port number to kill processes listening on.
37+
///
38+
/// # Returns
39+
///
40+
/// A `Result` containing a boolean value. If true, at least one process was killed; otherwise, false.
41+
pub fn kill_processes_by_port(port: u16) -> Result<bool, io::Error> {
42+
let process_infos = collect_proc();
43+
let mut killed = false;
44+
45+
for task in process_infos {
46+
let pid = task.pbsd.pbi_pid as i32;
47+
let mut kill_process = false;
48+
49+
let fds = listpidinfo::<ListFDs>(pid, task.pbsd.pbi_nfiles as usize);
50+
if let Ok(fds) = fds {
51+
for fd in fds {
52+
if let ProcFDType::Socket = fd.proc_fdtype.into() {
53+
if let Ok(socket) = pidfdinfo::<SocketFDInfo>(pid, fd.proc_fd) {
54+
match socket.psi.soi_kind.into() {
55+
SocketInfoKind::In => {
56+
if socket.psi.soi_protocol == libc::IPPROTO_UDP {
57+
let info = unsafe { socket.psi.soi_proto.pri_in };
58+
let local_port = u16::from_be(info.insi_lport as u16);
59+
if local_port == port {
60+
kill_process = true;
61+
break;
62+
}
63+
}
64+
}
65+
SocketInfoKind::Tcp => {
66+
let info = unsafe { socket.psi.soi_proto.pri_tcp };
67+
let local_port = u16::from_be(info.tcpsi_ini.insi_lport as u16);
68+
if local_port == port {
69+
kill_process = true;
70+
break;
71+
}
72+
}
73+
_ => (),
74+
}
75+
}
76+
}
77+
}
78+
}
79+
80+
if kill_process {
81+
debug!("Found process with PID {}", pid);
82+
let pid = Pid::from_raw(pid);
83+
let cmd = unsafe {
84+
CStr::from_ptr(task.pbsd.pbi_comm.as_ptr())
85+
.to_string_lossy()
86+
.into_owned()
87+
};
88+
89+
if cmd.starts_with("com.docker") {
90+
warn!("Warning: Found Docker. You might need to stop the container manually.");
91+
} else {
92+
info!("Killing process with PID {}", pid);
93+
match signal::kill(pid, Signal::SIGKILL) {
94+
Ok(_) => {
95+
killed = true;
96+
}
97+
Err(e) => {
98+
return Err(io::Error::new(
99+
io::ErrorKind::Other,
100+
format!("Failed to kill process {}: {}", pid, e),
101+
));
102+
}
103+
}
104+
}
105+
}
106+
}
107+
108+
Ok(killed)
109+
}

src/main.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
//! The utility accepts a list of port numbers as input and attempts to
55
//! terminate any processes listening on those ports.
66
7-
mod port;
8-
mod process;
7+
#[cfg(target_os = "linux")]
8+
mod linux;
9+
#[cfg(target_os = "macos")]
10+
mod macos;
11+
12+
#[cfg(target_os = "linux")]
13+
use linux::kill_processes_by_port;
14+
#[cfg(target_os = "macos")]
15+
use macos::kill_processes_by_port;
916

1017
use clap::Parser;
1118
use clap_verbosity_flag::{Verbosity, WarnLevel};
1219
use log::error;
13-
use port::kill_port;
1420
use std::process::exit;
1521

1622
/// The `KillPortArgs` struct is used to parse command-line arguments for the
@@ -19,7 +25,11 @@ use std::process::exit;
1925
#[command(author, version, about, long_about = None)]
2026
struct KillPortArgs {
2127
/// A list of port numbers to kill processes on.
22-
#[arg(name = "ports", help = "The list of port numbers to kill processes on")]
28+
#[arg(
29+
name = "ports",
30+
help = "The list of port numbers to kill processes on",
31+
required = true
32+
)]
2333
ports: Vec<u16>,
2434

2535
/// A verbosity flag to control the level of logging output.
@@ -51,7 +61,7 @@ fn main() {
5161

5262
// Attempt to kill processes listening on specified ports
5363
for port in args.ports {
54-
match kill_port(port) {
64+
match kill_processes_by_port(port) {
5565
Ok(killed) => {
5666
if killed {
5767
println!("Successfully killed process listening on port {}", port);

src/port.rs

Lines changed: 0 additions & 63 deletions
This file was deleted.

0 commit comments

Comments
 (0)