Skip to content

Commit 835789b

Browse files
committed
feat: refactoring, added ability to cancel scans, get cpu threads on app start up, improved error handling, allow port scans for port 0, added gh actions workflows
1 parent aaad53f commit 835789b

16 files changed

Lines changed: 391 additions & 96 deletions

File tree

.github/workflows/release.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
workflow_dispatch:
8+
9+
jobs:
10+
release:
11+
permissions:
12+
contents: write
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
platform: [ macos-latest, ubuntu-latest, windows-latest ]
17+
runs-on: ${{ matrix.platform }}
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
22+
- name: Install dependencies (ubuntu only)
23+
if: matrix.platform == 'ubuntu-latest'
24+
run: |
25+
sudo apt-get update
26+
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
27+
28+
- name: Node.js setup
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: 'lts/*'
32+
cache: 'yarn'
33+
34+
- name: Rust setup
35+
uses: dtolnay/rust-toolchain@stable
36+
37+
- name: Rust cache
38+
uses: swatinem/rust-cache@v2
39+
with:
40+
workspaces: './src-tauri -> target'
41+
42+
- name: Install app dependencies and build web
43+
run: yarn && yarn build
44+
45+
- name: Build the app
46+
uses: tauri-apps/tauri-action@v0
47+
48+
env:
49+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50+
with:
51+
tagName: ${{ github.ref_name }}
52+
releaseName: 'Advanced PassGen v__VERSION__'
53+
releaseBody: 'See the assets to download and install this version.'
54+
releaseDraft: true
55+
prerelease: false

.github/workflows/test.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Test
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- master
7+
- development
8+
9+
jobs:
10+
test-eslint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- name: Install modules
15+
run: yarn
16+
17+
- name: Run ESLint
18+
run: yarn run eslint . --ext .ts,.tsx,.js,.jsx
19+
20+
test-tauri:
21+
strategy:
22+
fail-fast: false
23+
matrix:
24+
platform: [ macos-latest, ubuntu-latest, windows-latest ]
25+
26+
runs-on: ${{ matrix.platform }}
27+
steps:
28+
- name: Checkout repository
29+
uses: actions/checkout@v4
30+
31+
- name: Install dependencies (ubuntu only)
32+
if: matrix.platform == 'ubuntu-latest'
33+
run: |
34+
sudo apt-get update
35+
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
36+
37+
- name: Setup node
38+
uses: actions/setup-node@v4
39+
with:
40+
node-version: 'lts/*'
41+
cache: 'yarn'
42+
43+
- name: Rust setup
44+
uses: dtolnay/rust-toolchain@stable
45+
46+
- name: Rust cache
47+
uses: swatinem/rust-cache@v2
48+
with:
49+
workspaces: './src-tauri -> target'
50+
51+
- name: Install app dependencies and build it
52+
run: yarn && yarn build
53+
54+
- uses: tauri-apps/tauri-action@v0
55+
env:
56+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Advanced PortChecker
22

3+
![Advanced PassGen](https://i.imgur.com/YazhCxS.png)
4+
5+
![GitHub](https://img.shields.io/badge/language-JavaScript+Rust-green)
6+
![GitHub](https://img.shields.io/github/license/CodeDead/Advanced-PortChecker)
7+
![GitHub release (latest by date)](https://img.shields.io/github/v/release/CodeDead/Advanced-PortChecker)
8+
39
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
410

511
Currently, two official plugins are available:

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "advanced-portchecker"
33
version = "0.1.0"
4-
description = "A Tauri App"
4+
description = "A lightweight TCP port checker with an intuitive UI."
55
authors = ["CodeDead <admin@codedead.com>"]
66
license = "GPL-3.0-only"
77
repository = "https://github.com/CodeDead/Advanced-PortChecker"

src-tauri/src/main.rs

Lines changed: 144 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,62 @@
22
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
33

44
use crate::result::{PortStatus, ScanResult};
5-
use std::net::{IpAddr, Shutdown, TcpStream, ToSocketAddrs, UdpSocket};
5+
use std::net::{IpAddr, Shutdown, TcpStream, ToSocketAddrs};
66
use std::ops::Deref;
7+
use std::sync::atomic::{AtomicBool, Ordering};
78
use std::sync::{Arc, Mutex};
89
use std::thread;
10+
use std::thread::available_parallelism;
911
use std::time::Duration;
1012

1113
mod result;
1214

15+
struct SharedState {
16+
is_scanning: Arc<AtomicBool>,
17+
cancellation_token: Arc<AtomicBool>,
18+
last_error: Arc<Mutex<String>>,
19+
}
20+
1321
fn main() {
22+
let shared_state = SharedState {
23+
is_scanning: Arc::new(AtomicBool::new(false)),
24+
cancellation_token: Arc::new(AtomicBool::new(false)),
25+
last_error: Arc::new(Mutex::new(String::from(""))),
26+
};
27+
1428
tauri::Builder::default()
15-
.invoke_handler(tauri::generate_handler![open_website, scan_port_range])
29+
.manage(shared_state)
30+
.invoke_handler(tauri::generate_handler![
31+
open_website,
32+
scan_port_range,
33+
cancel_scan,
34+
get_number_of_threads
35+
])
1636
.run(tauri::generate_context!())
1737
.expect("error while running tauri application");
1838
}
1939

40+
/// Get the number of threads that can be used for port scanning
41+
///
42+
/// # Returns
43+
///
44+
/// * `u32` - The number of threads that can be used for port scanning
45+
#[tauri::command]
46+
fn get_number_of_threads() -> usize {
47+
let default_parallelism_approx = available_parallelism().unwrap().get();
48+
default_parallelism_approx
49+
}
50+
51+
/// Open a website using the default browser
52+
///
53+
/// # Arguments
54+
///
55+
/// * `website` - The website that needs to be opened
56+
///
57+
/// # Returns
58+
///
59+
/// * `Ok(())` - If the website was opened successfully
60+
/// * `Err(String)` - If the website could not be opened
2061
#[tauri::command]
2162
fn open_website(website: &str) -> Result<(), String> {
2263
match open::that(website) {
@@ -25,15 +66,61 @@ fn open_website(website: &str) -> Result<(), String> {
2566
}
2667
}
2768

69+
/// Cancel a port scan
70+
///
71+
/// # Arguments
72+
///
73+
/// * `state` - The shared state that contains the cancellation token
74+
///
75+
/// # Returns
76+
///
77+
/// * `Ok(())` - If the cancellation token was set successfully
78+
/// * `Err(String)` - If the cancellation token could not be set
79+
#[tauri::command]
80+
async fn cancel_scan(state: tauri::State<'_, SharedState>) -> Result<(), String> {
81+
if !state.is_scanning.load(Ordering::SeqCst) {
82+
return Err(String::from("No scan is currently running"));
83+
}
84+
85+
state.cancellation_token.store(true, Ordering::SeqCst);
86+
state.is_scanning.store(false, Ordering::SeqCst);
87+
88+
Ok(())
89+
}
90+
91+
/// Scan a range of ports for a specified host
92+
///
93+
/// # Arguments
94+
///
95+
/// * `state` - The shared state that contains the cancellation token
96+
/// * `address` - The host that needs to be scanned
97+
/// * `start_port` - The initial port that needs to be scanned
98+
/// * `end_port` - The final port that needs to be scanned
99+
/// * `timeout` - The connection timeout (in milliseconds) before a port is marked as closed
100+
/// * `threads` - The number of threads that should be used to scan the ports
101+
/// * `sort` - Whether the results should be sorted by port number
102+
///
103+
/// # Returns
104+
///
105+
/// * `Ok(Vec<ScanResult>)` - If the scan was successful
106+
/// * `Err(String)` - If the scan was unsuccessful
28107
#[tauri::command]
29108
async fn scan_port_range(
109+
state: tauri::State<'_, SharedState>,
30110
address: &str,
31111
start_port: u16,
32112
end_port: u16,
33113
timeout: u64,
34114
threads: u32,
35115
sort: bool,
36116
) -> Result<Vec<ScanResult>, String> {
117+
if state.is_scanning.load(Ordering::SeqCst) {
118+
return Err(String::from("A scan is already running"));
119+
}
120+
121+
state.is_scanning.store(true, Ordering::SeqCst);
122+
state.last_error.lock().unwrap().clear();
123+
37124
let mut threads = threads;
38125
let all_results: Arc<Mutex<Vec<ScanResult>>> = Arc::new(Mutex::new(vec![]));
39126

@@ -57,8 +144,24 @@ async fn scan_port_range(
57144
let local_host = String::from(address);
58145

59146
let all_results = Arc::clone(&all_results);
147+
let cancellation_token = Arc::clone(&state.cancellation_token);
148+
let last_error = Arc::clone(&state.last_error);
149+
60150
let handle = thread::spawn(move || {
61-
let res = scan_tcp_range(&local_host, local_start, local_end, timeout);
151+
let res = scan_tcp_range(
152+
&local_host,
153+
local_start,
154+
local_end,
155+
timeout,
156+
cancellation_token,
157+
);
158+
159+
let res = res.unwrap_or_else(|e| {
160+
let mut last_error = last_error.lock().unwrap();
161+
*last_error = e.to_string();
162+
163+
vec![]
164+
});
62165

63166
let mut results = all_results.lock().unwrap();
64167
for l in res {
@@ -96,7 +199,16 @@ async fn scan_port_range(
96199
handle.join().unwrap();
97200
}
98201
} else {
99-
let res = scan_tcp_range(address, start_port, end_port, timeout);
202+
let cancellation_token = Arc::clone(&state.cancellation_token);
203+
204+
let res = scan_tcp_range(address, start_port, end_port, timeout, cancellation_token);
205+
let res = match res {
206+
Ok(res) => res,
207+
Err(e) => {
208+
return Err(e.to_string());
209+
}
210+
};
211+
100212
let mut all = all_results.lock().unwrap();
101213
for l in res {
102214
all.push(l);
@@ -110,6 +222,16 @@ async fn scan_port_range(
110222
res.sort_by(|a, b| a.port.cmp(&b.port));
111223
}
112224

225+
state.is_scanning.store(false, Ordering::SeqCst);
226+
state.cancellation_token.store(false, Ordering::SeqCst);
227+
228+
if res.is_empty() {
229+
let last_error = state.last_error.lock().unwrap();
230+
if !last_error.is_empty() {
231+
return Err(last_error.deref().to_string());
232+
}
233+
}
234+
113235
Ok(res.deref().to_vec())
114236
}
115237

@@ -121,17 +243,30 @@ async fn scan_port_range(
121243
/// * `start` - The initial port that needs to be scanned
122244
/// * `end` - The final port that needs to be scanned
123245
/// * `timeout` - The connection timeout (in milliseconds) before a port is marked as closed
124-
/// * `no_closed` - Sets whether closed ports should be added to the return list or not
246+
/// * `cancellation_token` - A cancellation token that can be used to cancel the scan
125247
fn scan_tcp_range(
126248
host: &str,
127249
start: u16,
128250
end: u16,
129251
timeout: u64,
130-
) -> Vec<ScanResult> {
252+
cancellation_token: Arc<AtomicBool>,
253+
) -> Result<Vec<ScanResult>, String> {
131254
let mut scan_result = vec![];
132255

133256
for n in start..=end {
134-
let mut address = format!("{}:{}", host, n).to_socket_addrs().unwrap();
257+
// Check the cancellation token and return if it's true
258+
if cancellation_token.load(Ordering::Relaxed) {
259+
return Ok(scan_result);
260+
}
261+
262+
let address = format!("{}:{}", host, n).to_socket_addrs();
263+
let mut address = match address {
264+
Ok(res) => res,
265+
Err(e) => {
266+
return Err(e.to_string());
267+
}
268+
};
269+
135270
let socket_address = address.next().unwrap();
136271
let ip_addr: IpAddr = socket_address.ip();
137272
let host_name = ip_addr.to_string();
@@ -146,7 +281,7 @@ fn scan_tcp_range(
146281
match res {
147282
Ok(_) => {}
148283
Err(e) => {
149-
panic!("Unable to shut down TcpStream: {}", e)
284+
println!("Unable to shut down TcpStream: {}", e)
150285
}
151286
}
152287
} else {
@@ -155,5 +290,5 @@ fn scan_tcp_range(
155290
}
156291
}
157292

158-
scan_result
293+
Ok(scan_result)
159294
}

src-tauri/src/result.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,19 @@ pub struct ScanResult {
2121
}
2222

2323
impl ScanResult {
24-
pub fn new(
25-
address: &str,
26-
port: u16,
27-
host_name: &str,
28-
port_status: PortStatus,
29-
) -> ScanResult {
24+
/// Create a new ScanResult
25+
///
26+
/// # Arguments
27+
///
28+
/// * `address` - The address that was scanned
29+
/// * `port` - The port that was scanned
30+
/// * `host_name` - The host name of the address that was scanned
31+
/// * `port_status` - The status of the port that was scanned
32+
///
33+
/// # Returns
34+
///
35+
/// A new ScanResult
36+
pub fn new(address: &str, port: u16, host_name: &str, port_status: PortStatus) -> ScanResult {
3037
let now = SystemTime::now();
3138
let now: DateTime<Utc> = now.into();
3239
let now = now.to_rfc3339();

0 commit comments

Comments
 (0)