Skip to content

Commit

Permalink
Merge pull request #3 from GyulyVGC/v0.2.0
Browse files Browse the repository at this point in the history
v0.2.0
  • Loading branch information
GyulyVGC authored Mar 27, 2024
2 parents 070cbed + 3225ed5 commit bb7ec89
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 38 deletions.
38 changes: 22 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
name: ${{ matrix.os }}-latest
runs-on: ${{ matrix.os }}-latest
strategy:
fail-fast: true
fail-fast: false
matrix:
include:
- os: ubuntu
Expand All @@ -31,25 +31,31 @@ jobs:
with:
components: rustfmt, clippy

# - name: Launch sshd (Linux)
# if: matrix.os == 'ubuntu'
# run: sudo systemctl start ssh
#
# - name: Launch sshd (macOS)
# if: matrix.os == 'macos'
# run: sudo systemsetup -setremotelogin on
#
# - name: Launch sshd (Windows)
# if: matrix.os == 'windows'
# run: |
# Get-Service -Name ssh-agent | Set-Service -StartupType Manual
# Start-Service ssh-agent

- name: fmt
run: cargo fmt --all -- --check
- name: build
run: cargo build --verbose
- name: clippy
run: cargo clippy -- -D warnings
- name: test
run: cargo test --verbose -- --nocapture

# - name: Install cargo-llvm-cov
# if: matrix.os == 'ubuntu'
# uses: taiki-e/install-action@cargo-llvm-cov
# - name: Generate code coverage
# if: matrix.os == 'ubuntu'
# run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
# - name: Upload coverage to Codecov
# if: matrix.os == 'ubuntu'
# uses: codecov/codecov-action@v4
# with:
# files: lcov.info
# fail_ci_if_error: false
# token: ${{ secrets.CODECOV_IO }}
- name: test (macOS)
if: matrix.os == 'macos'
run: sudo cargo test --verbose -- --nocapture

- name: test (Linux & Windows)
if: matrix.os == 'ubuntu' || matrix.os == 'windows'
run: cargo test --verbose -- --nocapture
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,18 @@

All releases with the relative changes are documented in this file.

## [0.2.0] - 2024-03-27
### Added
- New APIs to get the listening processes in a more granular way
- `get_ports_by_pid`
- `get_ports_by_process_name`
- `get_processes_by_port`
- New `Process` struct to represent a process identified by its PID and name
### Changed
- `Listener` struct now has a `process` field of type `Process`, which takes place of the old fields `pid` and `name`

## [0.1.0] - 2024-03-14
- Support for Windows, Linux and macOS
### Added
- Support for Windows, Linux and macOS
- `get_all` API to get all the listening processes

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "listeners"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
authors = ["Giuliano Bellini <[email protected]>"]
description = "Get processes listening on a TCP port in a cross-platform way"
Expand All @@ -9,7 +9,7 @@ repository = "https://github.com/GyulyVGC/listeners"
license = "MIT"
keywords = ["tcp", "listen", "port", "process"]
categories = ["network-programming"]
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md", "examples/**/*"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Some examples of existing libraries:
This library wants to fill this gap, and it aims to be:
- **Cross-platform**: it currently supports Windows, Linux and macOS
- **Performant**: it internally uses low-level system APIs
- **Simple**: it exposes a single API to get all the listening processes
- **Simple**: it exposes intuitive APIs to get details about the listening processes
- **Lightweight**: it has only the strictly necessary dependencies

## Roadmap
Expand All @@ -41,7 +41,7 @@ Add this to your `Cargo.toml`:
listeners = "0.1"
```

Get the listening processes:
Get all the listening processes:

``` rust
if let Ok(listeners) = listeners::get_all() {
Expand All @@ -61,3 +61,6 @@ PID: 160 Process name: mysqld Socket: [::]:3306
PID: 460 Process name: rapportd Socket: 0.0.0.0:50928
PID: 460 Process name: rapportd Socket: [::]:50928
```

For more examples of usage, including how to get listening processes in a more granular way,
check the [`examples`](https://github.com/GyulyVGC/listeners/tree/main/examples) folder.
1 change: 1 addition & 0 deletions examples/get_all.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
fn main() {
// Retrieve all listeners
if let Ok(listeners) = listeners::get_all() {
for l in listeners {
println!("{l}");
Expand Down
16 changes: 16 additions & 0 deletions examples/get_ports_by_pid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use std::env::args;

fn main() {
let pid = args()
.nth(1)
.expect("Expected CLI argument: PID")
.parse()
.expect("PID must be an unsigned integer on at most 32 bits");

// Retrieve ports listened to by a process given its PID
if let Ok(ports) = listeners::get_ports_by_pid(pid) {
for p in ports {
println!("{p}");
}
}
}
12 changes: 12 additions & 0 deletions examples/get_ports_by_process_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use std::env::args;

fn main() {
let process_name = args().nth(1).expect("Expected CLI argument: process name");

// Retrieve ports listened to by a process given its name
if let Ok(ports) = listeners::get_ports_by_process_name(&process_name) {
for p in ports {
println!("{p}");
}
}
}
16 changes: 16 additions & 0 deletions examples/get_processes_by_port.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use std::env::args;

fn main() {
let port = args()
.nth(1)
.expect("Expected CLI argument: port")
.parse()
.expect("Port must be an unsigned integer on at most 16 bits");

// Retrieve PID and name of processes listening on a given port
if let Ok(processes) = listeners::get_processes_by_port(port) {
for p in processes {
println!("{p}");
}
}
}
182 changes: 165 additions & 17 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,34 @@ mod platform;

type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

/// A process listening on a TCP port.
/// A process listening on a TCP socket.
#[derive(Eq, PartialEq, Hash, Debug)]
pub struct Listener {
/// The listening process.
pub process: Process,
/// The TCP socket this listener is listening on.
pub socket: SocketAddr,
}

/// A process, characterized by its PID and name.
#[derive(Eq, PartialEq, Hash, Debug)]
pub struct Process {
/// Process ID.
pub pid: u32,
/// Process name.
pub name: String,
/// The TCP socket this process is listening on.
pub socket: SocketAddr,
}

/// Returns the list of all processes listening on a TCP port.
/// Returns all the [Listener]s.
///
/// # Errors
///
/// This function returns an error if it fails to get the list of processes for the current platform.
/// This function returns an error if it fails to retrieve listeners for the current platform.
///
/// # Example
///
/// ``` rust
/// if let Ok(listeners) = listeners::get_all() {
/// for l in listeners {
/// println!("{l}");
/// }
/// }
/// ```
#[doc = include_str!("../examples/get_all.rs")]
/// ```
///
/// Output:
Expand All @@ -48,18 +51,163 @@ pub fn get_all() -> Result<HashSet<Listener>> {
platform::get_all()
}

/// Returns the list of [Process]es listening on a given TCP port.
///
/// # Arguments
///
/// * `port` - The TCP port to look for.
///
/// # Errors
///
/// This function returns an error if it fails to retrieve listeners for the current platform.
///
/// # Example
///
/// ``` no_run
#[doc = include_str!("../examples/get_processes_by_port.rs")]
/// ```
///
/// Output:
/// ``` text
/// PID: 160 Process name: mysqld
/// ```
pub fn get_processes_by_port(port: u16) -> Result<HashSet<Process>> {
platform::get_all().map(|listeners| {
listeners
.into_iter()
.filter(|listener| listener.socket.port() == port)
.map(|listener| listener.process)
.collect()
})
}

/// Returns the list of ports listened to by a process given its PID.
///
/// # Arguments
///
/// * `pid` - The PID of the process.
///
/// # Errors
///
/// This function returns an error if it fails to retrieve listeners for the current platform.
///
/// # Example
///
/// ``` no_run
#[doc = include_str!("../examples/get_ports_by_pid.rs")]
/// ```
///
/// Output:
/// ``` text
/// 3306
/// 33060
/// ```
pub fn get_ports_by_pid(pid: u32) -> Result<HashSet<u16>> {
platform::get_all().map(|listeners| {
listeners
.into_iter()
.filter(|listener| listener.process.pid == pid)
.map(|listener| listener.socket.port())
.collect()
})
}

/// Returns the list of ports listened to by a process given its name.
///
/// # Arguments
///
/// * `name` - The name of the process.
///
/// # Errors
///
/// This function returns an error if it fails to retrieve listeners for the current platform.
///
/// # Example
///
/// ``` no_run
#[doc = include_str!("../examples/get_ports_by_process_name.rs")]
/// ```
///
/// Output:
/// ``` text
/// 3306
/// 33060
/// ```
pub fn get_ports_by_process_name(name: &str) -> Result<HashSet<u16>> {
platform::get_all().map(|listeners| {
listeners
.into_iter()
.filter(|listener| listener.process.name == name)
.map(|listener| listener.socket.port())
.collect()
})
}

impl Listener {
fn new(pid: u32, name: String, socket: SocketAddr) -> Self {
Self { pid, name, socket }
let process = Process::new(pid, name);
Self { process, socket }
}
}

impl Process {
fn new(pid: u32, name: String) -> Self {
Self { pid, name }
}
}

impl Display for Listener {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"PID: {:<10} Process name: {:<25} Socket: {}",
self.pid, self.name, self.socket
)
let Listener { process, socket } = self;
let process = process.to_string();
write!(f, "{process:<55} Socket: {socket}",)
}
}

impl Display for Process {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Process { pid, name } = self;
write!(f, "PID: {pid:<10} Process name: {name}")
}
}

#[cfg(test)]
mod tests {
use crate::{Listener, Process};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};

#[test]
fn test_v4_listener_to_string() {
let listener = Listener::new(
455,
"rapportd".to_string(),
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 51189),
);
assert_eq!(
listener.to_string(),
"PID: 455 Process name: rapportd Socket: 0.0.0.0:51189"
);
}

#[test]
fn test_v6_listener_to_string() {
let listener = Listener::new(
160,
"mysqld".to_string(),
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 3306),
);
assert_eq!(
listener.to_string(),
"PID: 160 Process name: mysqld Socket: [::]:3306"
);
}

#[test]
fn test_process_to_string() {
let process = Process::new(611, "Microsoft SharePoint".to_string());
assert_eq!(
process.to_string(),
"PID: 611 Process name: Microsoft SharePoint"
);
}
}
Loading

0 comments on commit bb7ec89

Please sign in to comment.