Skip to content

Instantly share code, notes, and snippets.

@picatz
Last active June 7, 2018 22:34
Show Gist options
  • Save picatz/e8a6f526ce236481837f3cdbb9b84152 to your computer and use it in GitHub Desktop.
Save picatz/e8a6f526ce236481837f3cdbb9b84152 to your computer and use it in GitHub Desktop.
asynchronous port scanner in ruby
require 'async/io'
require 'async/await'
require 'async/semaphore'
class PortScanner
include Async::Await
include Async::IO
def initialize(host: '127.0.0.1', ports:)
@host = host
@ports = ports
@semaphore = Async::Semaphore.new(`ulimit -n`.to_i)
end
def scan_port(port, timeout: 0.5)
timeout(timeout) do
Async::IO::Endpoint.tcp(@host, port).connect do |peer|
peer.close
puts "#{port} open"
end
end
rescue Errno::ECONNREFUSED, Async::TimeoutError
puts "#{port} closed"
rescue Errno::EMFILE
sleep timeout
retry
end
async def start(timeout: 0.5)
@ports.map do |port|
@semaphore.async do
scan_port(port, timeout: timeout)
end
end.collect(&:result)
end
end
scanner = PortScanner.new(ports: (1..1024))
scanner.start
@ioquatix
Copy link

ioquatix commented Jun 7, 2018

To add synchronisation to the port scanner, try adding

@ports.map{|port| scan_port(port, timeout: timeout)}.collect(&:result)

Then, instead of printing the result you can return it.

@picatz
Copy link
Author

picatz commented Jun 7, 2018

The syncing part is pretty cool! Not totally needed for my cases, but a really nice touch. 👌

@ioquatix
Copy link

ioquatix commented Jun 7, 2018

@picatz Because of the way async works - scanner.start will wait until all tasks complete anyway.

If you add

require 'async/semaphore'

def initialize
    @semaphore = Async::Semaphore.new(`ulimit -n`.to_i)
...
end

# remove async from def scan_port

async def start(timeout: 0.5)
    @ports.map{|port|
        @semaphore.async do
            scan_port(port, timeout: timeout)}
        end
    end.collect(&:result)
end

You should avoid the need for handling Errno::EMFILE.

You need to use async v1.9.1 for this to work.

@picatz
Copy link
Author

picatz commented Jun 7, 2018

@ioquatix Async::Semaphore seems to be a nice abstraction to help clean up the code and provide a similar semantic for people that understand Ruby's multi-threading APIs. 👌

However, I still seem to reach the too many files limit since, I'd assume, other operations on my system have files that are open. So, I still end up hitting Errno::EMFILE, or using it to keep track of the situation myself with raise Errno::EMFILE while @open_fds >= @fd_limit which I don't mind doing in this case -- I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment