├── lib └── fosl │ ├── namespace.rb │ ├── process.rb │ └── parser.rb ├── Rakefile ├── examples ├── test.rb ├── lsof.rb └── lsofpid.rb ├── fosl.gemspec └── README.md /lib/fosl/namespace.rb: -------------------------------------------------------------------------------- 1 | module FOSL 2 | end 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => [:package] 2 | 3 | task :test do 4 | system("cd test; ruby alltests.rb") 5 | end 6 | 7 | task :package => [:test, :package_real] do 8 | end 9 | 10 | task :package_real do 11 | system("gem build fosl.gemspec") 12 | end 13 | 14 | task :publish do 15 | latest_gem = %x{ls -t fosl*.gem}.split("\n").first 16 | system("gem push #{latest_gem}") 17 | end 18 | -------------------------------------------------------------------------------- /examples/test.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "ap" 3 | require "fosl/parser" 4 | 5 | a = FOSL::Parser.new 6 | data = a.lsof("-nP") 7 | 8 | # Show any process with listening sockets: 9 | data.map do |pid, process| 10 | next if process.listeners.empty? 11 | 12 | ap :pid => pid, 13 | :command => process.command, 14 | :listeners => process.listeners 15 | end 16 | 17 | -------------------------------------------------------------------------------- /examples/lsof.rb: -------------------------------------------------------------------------------- 1 | 2 | $: << File.join(File.dirname(__FILE__), "..", "lib") 3 | 4 | require "rubygems" 5 | require "fosl/parser" 6 | 7 | if ARGV.size == 0 8 | $stderr.puts "Usage: #{$0} pid [pid ...]" 9 | exit 1 10 | end 11 | 12 | 13 | lsof = FOSL::Parser.new 14 | results = lsof.lsof(ARGV.join(" ")) 15 | 16 | results.each do |pid, process| 17 | process.files.each do |file| 18 | p pid => file 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/fosl/process.rb: -------------------------------------------------------------------------------- 1 | require "fosl/namespace" 2 | 3 | class FOSL::Process 4 | attr_reader :files 5 | attr_reader :pid 6 | attr_accessor :command 7 | attr_accessor :login 8 | 9 | def initialize(pid) 10 | @command = nil 11 | @login = nil 12 | @pid = pid 13 | @files = [] 14 | end 15 | 16 | # helpers 17 | def listeners 18 | @files.find_all { |f| f[:state] == "LISTEN" } 19 | end 20 | end # class FOSL::Process 21 | -------------------------------------------------------------------------------- /examples/lsofpid.rb: -------------------------------------------------------------------------------- 1 | 2 | $: << File.join(File.dirname(__FILE__), "..", "lib") 3 | 4 | require "rubygems" 5 | require "fosl/parser" 6 | 7 | if ARGV.size == 0 8 | $stderr.puts "Usage: #{$0} pid [pid ...]" 9 | exit 1 10 | end 11 | 12 | 13 | lsof = FOSL::Parser.new 14 | pidflags = ARGV.collect { |a| "-p #{a}" }.join(" ") 15 | results = lsof.lsof("-nP #{pidflags}") 16 | 17 | results.each do |pid, process| 18 | process.files.each do |file| 19 | p pid => file 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /fosl.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | files = [] 3 | dirs = %w{lib samples test bin} 4 | dirs.each do |dir| 5 | files += Dir["#{dir}/**/*"] 6 | end 7 | 8 | spec.name = "fosl" 9 | spec.version = "0.0.2" 10 | spec.summary = "fosl - a ruby api for reading lsof(1) output" 11 | spec.description = "" 12 | spec.files = files 13 | spec.require_paths << "lib" 14 | 15 | spec.author = "Jordan Sissel" 16 | spec.email = "jls@semicomplete.com" 17 | spec.homepage = "https://github.com/jordansissel/ruby-lsof" 18 | end 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fosl, a Ruby lsof API 2 | 3 | Status: 4 | 5 | The API works now, but is less a true API and more a light hash-ish wrapping on 6 | lsof output. 7 | 8 | ## Example: 'lsof.rb' 9 | 10 | Code: 11 | 12 | This example runs 'lsof' like you would normally, but captures the output into 13 | ruby. 14 | 15 | % sudo ruby lsof.rb -i :80 -s TCP:LISTEN 16 | {1846=>{:type=>"IPv4", :send_queue=>"0", :protocol=>"TCP", :state=>"LISTEN", :name=>"*:www", :fd=>7, :read_queue=>"0"}} 17 | {1847=>{:type=>"IPv4", :send_queue=>"0", :protocol=>"TCP", :state=>"LISTEN", :name=>"*:www", :fd=>7, :read_queue=>"0"}} 18 | 19 | % ps -fp 1846 -p 1847 20 | UID PID PPID C STIME TTY TIME CMD 21 | root 1846 1 0 Mar08 ? 00:00:00 nginx: master process /usr/sbin/nginx 22 | www-data 1847 1846 0 Mar08 ? 00:00:01 nginx: worker process 23 | 24 | ## Example: What files do I have open? 25 | 26 | Code: 27 | 28 | Output: 29 | 30 | % sudo ruby lsofpid.rb 1 31 | {1=>{:fd=>"cwd", :name=>"/"}} 32 | {1=>{:fd=>"rtd", :name=>"/"}} 33 | {1=>{:fd=>"txt", :name=>"/sbin/init"}} 34 | {1=>{:fd=>"mem", :name=>"/lib/libnss_files-2.11.1.so"}} 35 | ... output omitted ... 36 | {1=>{:fd=>"mem", :name=>"/lib/ld-2.11.1.so"}} 37 | {1=>{:fd=>0, :name=>"/dev/null"}} 38 | {1=>{:fd=>1, :name=>"/dev/null"}} 39 | {1=>{:fd=>2, :name=>"/dev/null"}} 40 | {1=>{:fd=>3, :name=>"pipe"}} 41 | {1=>{:fd=>4, :name=>"pipe"}} 42 | {1=>{:fd=>5, :name=>"inotify"}} 43 | {1=>{:fd=>6, :name=>"inotify"}} 44 | {1=>{:fd=>7, :name=>"socket"}} 45 | {1=>{:fd=>8, :name=>"socket"}} 46 | {1=>{:fd=>9, :name=>"socket"}} 47 | {1=>{:fd=>10, :name=>"socket"}} 48 | 49 | 50 | ## Example usage (Show all listeners): 51 | 52 | require "rubygems" 53 | require "ap" 54 | require "fosl/parser" 55 | 56 | a = LSOF::Parser.new 57 | data = a.lsof("-nP") # runs "lsof -nP", roughly. 58 | 59 | # Show any process with listening sockets: 60 | data.map do |pid, process| 61 | next if process.listeners.empty? 62 | # 'process' here is an instance of LSOF::Process 63 | 64 | ap :pid => pid, 65 | :command => process.command, 66 | :listeners => process.listeners 67 | end 68 | 69 | Sample output: 70 | 71 | { 72 | :command => "smbd", 73 | :pid => 1007, 74 | :listeners => [ 75 | [0] { 76 | :protocol => "TCP", 77 | :state => "LISTEN", 78 | :fd => 22, 79 | :read_queue => "0", 80 | :name => "*:445", 81 | :send_queue => "0" 82 | }, 83 | [1] { 84 | :protocol => "TCP", 85 | :state => "LISTEN", 86 | :fd => 23, 87 | :read_queue => "0", 88 | :name => "*:139", 89 | :send_queue => "0" 90 | } 91 | ] 92 | } 93 | { 94 | :command => "nginx", 95 | :pid => 1846, 96 | :listeners => [ 97 | [0] { 98 | :protocol => "TCP", 99 | :state => "LISTEN", 100 | :fd => 7, 101 | :read_queue => "0", 102 | :name => "*:80", 103 | :send_queue => "0" 104 | } 105 | ] 106 | } 107 | 108 | -------------------------------------------------------------------------------- /lib/fosl/parser.rb: -------------------------------------------------------------------------------- 1 | require "fosl/namespace" 2 | require "fosl/process" 3 | 4 | class FOSL::Parser 5 | # Fields are separated by newlines or null 6 | # Fields start with a character followed by the data 7 | 8 | # These are the fields according to the lsof manpage. 9 | # If you want to implement one, you should write a 'parse_' 10 | # method. It should return a hash of key => value you want to save. 11 | # 12 | # # The following copied mostly verbatim from the lsof manpage. 13 | # - a file access mode 14 | # - c process command name (all characters from proc or user structure) 15 | # - C file structure share count 16 | # - d file's device character code 17 | # - D file's major/minor device number (0x) 18 | # - f file descriptor 19 | # - F file structure address (0x) 20 | # - G file flaGs (0x; names if +fg follows) 21 | # - i file's inode number 22 | # - k link count 23 | # - l file's lock status 24 | # - L process login name 25 | # - m marker between repeated output 26 | # - n file name, comment, Internet address 27 | # - N node identifier (ox 28 | # - o file's offset (decimal) 29 | # - p process ID (always selected) 30 | # - g process group ID 31 | # - P protocol name 32 | # - r raw device number (0x) 33 | # - R parent process ID 34 | # - s file's size (decimal) 35 | # - S file's stream identification 36 | # - t file's type 37 | # - T TCP/TPI information, identified by prefixes (the 38 | # - u process user ID 39 | # - z Solaris 10 and higher zone name 40 | # - Z SELinux security context (inhibited when SELinux is disabled) 41 | 42 | # T is various network/tcp/socket information. 43 | def parse_T(data) 44 | prefix, value = data.split("=") 45 | case prefix 46 | when "ST" ; prefix = :state 47 | when "QR" ; prefix = :read_queue 48 | when "QS" ; prefix = :send_queue 49 | 50 | # (sissel) I don't know the values of these fields. Feel free 51 | # to implement them and send me patches. 52 | #when "SO" ; prefix = :socket_options 53 | #when "SS" ; prefix = :socket_State 54 | #when "TF" ; prefix = :tcp_flags 55 | #when "WR" ; prefix = :read_window 56 | #when "WW" ; prefix = :write_window 57 | end 58 | return { prefix => value } 59 | end # def parse_T 60 | 61 | # The file's type 62 | def parse_t(data) 63 | return { :type => data } 64 | end 65 | 66 | # The protocol name 67 | def parse_P(data) 68 | return { :protocol => data } 69 | end 70 | 71 | # the pid 72 | def parse_p(data) 73 | new_pid(data.to_i) 74 | return :new_pid 75 | end 76 | 77 | # the file name or identifier 78 | def parse_n(data) 79 | return { :name => data } 80 | end 81 | 82 | # file descriptor (or 'cwd' etc...) 83 | def parse_f(data) 84 | new_file 85 | 86 | # Convert to int it looks like a number. 87 | if data.to_i != 0 or data == "0" 88 | data = data.to_i 89 | end 90 | 91 | return { :fd => data } 92 | end 93 | 94 | # The command name 95 | def parse_c(data) 96 | @current_process.command = data 97 | return nil 98 | end 99 | 100 | # The login name 101 | def parse_L(data) 102 | @current_process.login = data 103 | return nil 104 | end 105 | 106 | # state helper, creates a new process 107 | def new_pid(pid) 108 | new_file # push the last file (if any) onto the last process 109 | @current_process = FOSL::Process.new(pid) 110 | end 111 | 112 | # state helper, creates a new file hash 113 | def new_file 114 | if !@current_file.nil? && !@current_file.empty? 115 | @current_process.files << @current_file 116 | end 117 | 118 | @current_file = {} 119 | end 120 | 121 | # Parse output from an lsof(1) run. You must run 122 | # This output must be from lsof run with this flag '-F Pcfnt0' 123 | def parse(data) 124 | if data[0..0] != "p" 125 | raise "Expected first character to be 'p'. Unexpected data input - #{data[0..30]}..." 126 | end 127 | 128 | result = Hash.new { |h,k| h[k] = FOSL::Process.new(k) } 129 | 130 | data.split(/[\n\0]/).each do |field| 131 | next if field.empty? 132 | type = field[0 .. 0] 133 | value = field[1 .. -1] 134 | 135 | method = "parse_#{type}".to_sym 136 | if self.respond_to?(method) 137 | r = self.send(method, value) 138 | #p field => r 139 | if r.is_a?(Hash) 140 | @current_file.merge!(r) 141 | elsif r == :new_pid 142 | result[@current_process.pid] = @current_process 143 | end 144 | else 145 | $stderr.puts "Unsupported field type '#{type}': #{field.inspect}" 146 | end 147 | end 148 | 149 | # push last file 150 | new_file 151 | 152 | return result 153 | end # def parse 154 | 155 | # Helper for running lsof. 156 | # Returns the same thing as 'parse' 157 | # 158 | # Example: 159 | # lsof("-i :443") 160 | def lsof(args="") 161 | output = `lsof -F PcfntT0L #{args}` 162 | # Should we raise an exception, or just return empty results, on failure? 163 | if $?.exitstatus != 0 164 | raise "lsof exited with status #{$?.exitstatus}" 165 | end 166 | return self.parse(output) 167 | end 168 | end # class FOSL::Parser 169 | --------------------------------------------------------------------------------