The next version switches to using concurrent_forall/3. Since this predicate is so new (3 days old) and I run on Windows, I installed the Windows 64-bit version of the daily build.
SWI-Prolog (threaded, 64 bits, version 8.3.2-198-gd839164c7)
Note: This code also moved executing debug/1
on the command line into a Prolog directive
:- debug(concurrent).
which can easily be commented out. If you peruse the SWI-Prolog source code on GitHub you will often find these lines commented out.
Also notice how much simpler the code becomes when using concurrent_forall/3.
The hardest part about writing this was trying to understand concurrent_forall(:Generate, :Test)
, how did Generate
and Test
align with my existing code. So instead of trying to understand the code from the top down I looked at the critical predicate common to all of this which is thread_create/2 and in concurrent_forall/2 is in the line maplist(thread_create(fa_worker(Q, Me, Templ, Test)), Workers)
and just figured out which variables I could set and what they needed. The only one that can be set by calling concurrent_forall/3 is Test
which needs to be port_scan(IP_address,Port)
, after that identifying what the rest of concurrent_forall needed was easy.
For this use of concurrent_forall instead of concurrent_forall(:Generate, :Test)
I think of it as concurrent_forall(:Generate unique threads values, :Call thread with unique values)
.
:- debug(concurrent).
concurrent_scan(IP_address,Low_port,High_port,Number_of_threads) :-
concurrent_forall(
between(Low_port,High_port,Port),
port_scan(IP_address,Port),
[threads(Number_of_threads)]
).
port_scan(IP_address,Port) :-
catch(
setup_call_cleanup(
tcp_socket(Socket),
(
% Open stream socket based on TCP/IP which uses IP address and port number, i.e. INET socket
tcp_connect(Socket, IP_address:Port),
format('Port ~w: open~n', [Port])
),
tcp_close_socket(Socket)
),
error(_,_),
true
).
Example run
?- concurrent_scan('140.211.166.101',75,84,3).
% [Thread 5] Running test user:port_scan('140.211.166.101',77)
% [Thread 3] Running test user:port_scan('140.211.166.101',76)
% [Thread 4] Running test user:port_scan('140.211.166.101',75)
% [Thread 5] Running test user:port_scan('140.211.166.101',78)
% [Thread 4] Running test user:port_scan('140.211.166.101',79)
% [Thread 3] Running test user:port_scan('140.211.166.101',80)
Port 80: open
% [Thread 3] Running test user:port_scan('140.211.166.101',81)
% [Thread 5] Running test user:port_scan('140.211.166.101',82)
% [Thread 4] Running test user:port_scan('140.211.166.101',83)
% [Thread 3] Running test user:port_scan('140.211.166.101',84)
true.
NB The threads are being reused this time.
A more comprehensive example. (debug/1 was commented out.)
?- time(concurrent_scan('140.211.166.101',1,65536,8192)).
Port 80: open
Port 22: open
Port 443: open
% 196,643 inferences, 0.500 CPU in 174.439 seconds (0% CPU, 393286 Lips)
true.
8192 threads checking 65536 ports in ~3 minutes.
The comment Jan W. made about
and that I asked about now makes more sense. If you read the code for concurrent_forall you will notice that to pass messages back would require adding more complexity to something that is already very complex. So I take it to mean that if you want to use concurrent_forall then use it as designed, even it is breaking some rules of thumb such as have the threads know as little as possible about the outside world.