How to make net-ping thread safe?

Discussion in 'Ruby' started by Laurent Julliard, Mar 15, 2008.

  1. Hi,

    I'm using the excellent net-ping gem (v 1.2.2 on Ruby 1.8.6 Linux box)
    from Daniel Berger to ping a number of machines on a local area network.

    I recently refactored my code to use threads and I'm having troubles
    with Net::ping::ICMP. I'm trying to ping several machines, each one in a
    separate thread and I'm seing strange things like when one machine is on
    all three machines are reported on (although two are off). When I run my
    program with one thread only the on/off state shows as expected.

    So I'm wondering if the Net::ping::ICMP#ping method is thread safe?
    Daniel said he made no attempt to make sure that the code is and
    usggested that I post this question here.

    The code for the method Net::ping::ICMP#ping is here:
    http://www.koders.com/ruby/fidE6256BC790B8AD197544CE26287B5C7D3200E4C8.aspx?s=icmp#L5

    (line 61)

    Here is also a simplified version of my own code:

    -----------------------------------------------

    require 'rubygems'
    require 'net/ping/icmp'

    WORK_STATIONS = ['192.168.1.1', '192.168.1.2', '192.168.1.3']
    threads = []
    ping_objects = []

    WORK_STATIONS.each do |ip|
    ping_objects << Net::ping::ICMP.new(ip)
    threads << Thread.new(ip, ping_objects.last) do |ip, p|
    puts "Monitoring #{ip}..."
    loop do
    puts "#{ip} is #{p.ping ? 'on' : 'off'}"
    sleep 2
    end
    end
    end

    threads.each { |th| th.join }

    ------------------------------------------------

    Any advice either to change my own script or improve the net-ping code
    is welcome.

    Thanks for your help!

    Laurent
    Laurent Julliard, Mar 15, 2008
    #1
    1. Advertising

  2. Laurent Julliard wrote:
    > Hi,
    >
    > I'm using the excellent net-ping gem (v 1.2.2 on Ruby 1.8.6 Linux box)
    > from Daniel Berger to ping a number of machines on a local area network.
    >
    > I recently refactored my code to use threads and I'm having troubles
    > with Net::ping::ICMP. I'm trying to ping several machines, each one in a
    > separate thread and I'm seing strange things like when one machine is on
    > all three machines are reported on (although two are off). When I run my
    > program with one thread only the on/off state shows as expected.
    >
    > So I'm wondering if the Net::ping::ICMP#ping method is thread safe?
    > Daniel said he made no attempt to make sure that the code is and
    > usggested that I post this question here.
    >
    > The code for the method Net::ping::ICMP#ping is here:
    > http://www.koders.com/ruby/fidE6256BC790B8AD197544CE26287B5C7D3200E4C8.aspx?s=icmp#L5
    >
    >
    > (line 61)
    >
    > Here is also a simplified version of my own code:
    >
    > -----------------------------------------------
    >
    > require 'rubygems'
    > require 'net/ping/icmp'
    >
    > WORK_STATIONS = ['192.168.1.1', '192.168.1.2', '192.168.1.3']
    > threads = []
    > ping_objects = []
    >
    > WORK_STATIONS.each do |ip|
    > ping_objects << Net::ping::ICMP.new(ip)
    > threads << Thread.new(ip, ping_objects.last) do |ip, p|
    > puts "Monitoring #{ip}..."
    > loop do
    > puts "#{ip} is #{p.ping ? 'on' : 'off'}"
    > sleep 2
    > end
    > end
    > end
    >
    > threads.each { |th| th.join }
    >
    > ------------------------------------------------
    >
    > Any advice either to change my own script or improve the net-ping code
    > is welcome.
    >
    > Thanks for your help!
    >
    > Laurent
    >


    In icmp.rb, the problem seems to be that it uses process ID as the ICMP
    packet ID, so it is impossible to tell which ECHO REPLY packet
    corresponds to which request (if multiple requests come from one process).

    Quoting http://tools.ietf.org/html/rfc792:

    The identifier and sequence number may be used by the echo sender
    to aid in matching the replies with the requests. For example,
    the identifier might be used like a port in TCP or UDP to identify
    a session, and the sequence number might be incremented on each
    request sent. The destination returns these same values in the
    reply.

    Maybe it is standard practice to use pid, so that multiple ICMP clients
    (e.g. /bin/ping) never step on each other. In that case, I guess icmp.rb
    could fork{sleep} for each Ping instance and use the child pid as a
    conflict-free identifier.[1]

    However, watching tcpdump as /bin/ping is running on linux, it seems
    that the ID might be a local port rather than process id. Maybe ping is
    just binding a socket to 0 to ask the OS for a unique port.

    Ruby's icmp should do whatever /bin/ping does.

    You could work around this by filtering the response by sender host,
    *if* you assume that different threads never ping the same host.

    Or you could just shell out to ping (which is what I always do).

    [1] http://www.ping127001.com/pingpage/ping.html uses PID.

    --
    vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407
    Joel VanderWerf, Mar 15, 2008
    #2
    1. Advertising

  3. Joel VanderWerf wrote:
    > In icmp.rb, the problem seems to be that it uses process ID as the ICMP
    > packet ID, so it is impossible to tell which ECHO REPLY packet
    > corresponds to which request (if multiple requests come from one process).


    On second thought, the problem could be solved in icmp.rb by using the
    sequence number to separate requests, and keeping the sequence number in
    an ICMP class var instead of instance vars. Here's a patch that seems to
    make your original code work correctly (detecting some hosts up and some
    down).

    ---
    /usr/local/lib/ruby/gems/1.8/gems/net-ping-1.2.2/lib/net/ping/icmp.rb
    2008-01-23 19:46:14.000000000 -0800
    +++ net/ping/icmp.rb 2008-03-15 13:09:55.000000000 -0700
    @@ -24,7 +24,6 @@
    def initialize(host=nil, port=nil, timeout=5)
    raise 'requires root privileges' if Process.euid > 0

    - @seq = 0
    @bind_port = 0
    @bind_host = nil
    @data_size = 56
    @@ -37,6 +36,11 @@
    super(host, port, timeout)
    @port = nil # This value is not used in ICMP pings.
    end
    +
    + @seq = 0
    + def self.next_seq
    + @seq = (@seq + 1) % 65536
    + end

    # Sets the number of bytes sent in the ping method.
    #
    @@ -73,14 +77,14 @@
    socket.bind(saddr)
    end

    - @seq = (@seq + 1) % 65536
    + seq = ICMP.next_seq
    pstring = 'C2 n3 A' << @data_size.to_s
    timeout = @timeout

    checksum = 0
    - msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, @seq,
    @data].pack(pstring)
    + msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, seq,
    @data].pack(pstring)
    checksum = checksum(msg)
    - msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, @seq,
    @data].pack(pstring)
    + msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, seq,
    @data].pack(pstring)

    start_time = Time.now

    @@ -101,7 +105,7 @@
    end

    pid = nil
    - seq = nil
    + rcv_seq = nil

    data, sender = socket.recvfrom(1500)
    port, host = Socket.unpack_sockaddr_in(sender)
    @@ -110,15 +114,15 @@
    case type
    when ICMP_ECHOREPLY
    if data.length >= 28
    - pid, seq = data[24, 4].unpack('n3')
    + pid, rcv_seq = data[24, 4].unpack('n3')
    end
    else
    if data.length > 56
    - pid, seq = data[52, 4].unpack('n3')
    + pid, rcv_seq = data[52, 4].unpack('n3')
    end
    end

    - if pid == @pid && seq == @seq && type == ICMP_ECHOREPLY
    + if pid == @pid && rcv_seq == seq && type == ICMP_ECHOREPLY
    bool = true
    end
    }


    --
    vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407
    Joel VanderWerf, Mar 15, 2008
    #3
  4. Forgot to make the previous patch thread safe:

    --- net/ping/icmp.rb.bck 2008-03-15 13:19:42.000000000 -0700
    +++ net/ping/icmp.rb 2008-03-15 13:19:42.000000000 -0700
    @@ -1,5 +1,6 @@
    $LOAD_PATH.unshift File.dirname(__FILE__)
    require 'ping'
    +require 'thread'

    module Net
    class Ping::ICMP < Ping
    @@ -38,8 +39,11 @@
    end

    @seq = 0
    + @seq_mutex = Mutex.new
    def self.next_seq
    - @seq = (@seq + 1) % 65536
    + @seq_mutex.synchronize do
    + @seq = (@seq + 1) % 65536
    + end
    end

    # Sets the number of bytes sent in the ping method.

    --
    vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407
    Joel VanderWerf, Mar 15, 2008
    #4
  5. On Mar 15, 11:28 am, Laurent Julliard <> wrote:
    > Hi,
    >
    > I'm using the excellent net-ping gem (v 1.2.2 on Ruby 1.8.6 Linux box)
    > from Daniel Berger to ping a number of machines on a local area network.
    >
    > I recently refactored my code to use threads and I'm having troubles
    > with Net::ping::ICMP. I'm trying to ping several machines, each one in a
    > separate thread and I'm seing strange things like when one machine is on
    > all three machines are reported on (although two are off). When I run my
    > program with one thread only the on/off state shows as expected.
    >
    > So I'm wondering if the Net::ping::ICMP#ping method is thread safe?


    <snip>

    Hi Laurent,

    Please apply Joel's patches and let me know how it works. If all is
    well I'll apply the patches and put out another release.

    BTW, thanks Joel!

    Regards,

    Dan
    Daniel Berger, Mar 15, 2008
    #5
  6. Joel VanderWerf wrote:
    > Joel VanderWerf wrote:
    >> In icmp.rb, the problem seems to be that it uses process ID as the
    >> ICMP packet ID, so it is impossible to tell which ECHO REPLY packet
    >> corresponds to which request (if multiple requests come from one
    >> process).

    >
    > On second thought, the problem could be solved in icmp.rb by using the
    > sequence number to separate requests, and keeping the sequence number in
    > an ICMP class var instead of instance vars. Here's a patch that seems to
    > make your original code work correctly (detecting some hosts up and some
    > down).
    >


    Joel,

    Fist of all thanks for catching this!

    Your recommendation is to turn the sequence number into a class variable
    so shouldn't the sequence number really be named @@seq rather than @seq
    if we want the sequence number to be unique across multiple instances of
    ICMP objects?

    And if this is so, one of the consequence is that for a given ICMP
    instance chances are that the seq field in the icmp packet will not
    contain consecutive values. I don't know if this is a problem wrt to the
    ICMP specifications or not.

    Don't you think an alternative would be not to use the PID as the packet
    identifier but rather a class variable of our own that we would
    increment each time a new ICMP instance is created?

    Thanks again for your comments.

    Laurent



    > ---
    > /usr/local/lib/ruby/gems/1.8/gems/net-ping-1.2.2/lib/net/ping/icmp.rb
    > 2008-01-23 19:46:14.000000000 -0800
    > +++ net/ping/icmp.rb 2008-03-15 13:09:55.000000000 -0700
    > @@ -24,7 +24,6 @@
    > def initialize(host=nil, port=nil, timeout=5)
    > raise 'requires root privileges' if Process.euid > 0
    >
    > - @seq = 0
    > @bind_port = 0
    > @bind_host = nil
    > @data_size = 56
    > @@ -37,6 +36,11 @@
    > super(host, port, timeout)
    > @port = nil # This value is not used in ICMP pings.
    > end
    > +
    > + @seq = 0
    > + def self.next_seq
    > + @seq = (@seq + 1) % 65536
    > + end
    >
    > # Sets the number of bytes sent in the ping method.
    > #
    > @@ -73,14 +77,14 @@
    > socket.bind(saddr)
    > end
    >
    > - @seq = (@seq + 1) % 65536
    > + seq = ICMP.next_seq
    > pstring = 'C2 n3 A' << @data_size.to_s
    > timeout = @timeout
    >
    > checksum = 0
    > - msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, @seq,
    > @data].pack(pstring)
    > + msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, seq,
    > @data].pack(pstring)
    > checksum = checksum(msg)
    > - msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, @seq,
    > @data].pack(pstring)
    > + msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, seq,
    > @data].pack(pstring)
    >
    > start_time = Time.now
    >
    > @@ -101,7 +105,7 @@
    > end
    >
    > pid = nil
    > - seq = nil
    > + rcv_seq = nil
    >
    > data, sender = socket.recvfrom(1500)
    > port, host = Socket.unpack_sockaddr_in(sender)
    > @@ -110,15 +114,15 @@
    > case type
    > when ICMP_ECHOREPLY
    > if data.length >= 28
    > - pid, seq = data[24, 4].unpack('n3')
    > + pid, rcv_seq = data[24, 4].unpack('n3')
    > end
    > else
    > if data.length > 56
    > - pid, seq = data[52, 4].unpack('n3')
    > + pid, rcv_seq = data[52, 4].unpack('n3')
    > end
    > end
    >
    > - if pid == @pid && seq == @seq && type == ICMP_ECHOREPLY
    > + if pid == @pid && rcv_seq == seq && type == ICMP_ECHOREPLY
    > bool = true
    > end
    > }
    >
    >
    Laurent Julliard, Mar 16, 2008
    #6
  7. 2008/3/16, Laurent Julliard <>:

    > Your recommendation is to turn the sequence number into a class
    > variable so shouldn't the sequence number really be named @@seq
    > rather than @seq if we want the sequence number to be unique across
    > multiple instances of ICMP objects?


    If I am correct, Joel is using a ICMP class instance variable instead
    of a class variable. So in the patch, @seq is not a var of an ICMP
    object but an instance variable of ICMP class (as an object :) ).

    -- Jean-Fran=E7ois.
    Jean-François Trân, Mar 16, 2008
    #7
  8. Laurent Julliard wrote:
    > Hi,
    >
    > I'm using the excellent net-ping gem (v 1.2.2 on Ruby 1.8.6 Linux box)
    > from Daniel Berger to ping a number of machines on a local area network.
    >
    > I recently refactored my code to use threads and I'm having troubles
    > with Net::ping::ICMP. I'm trying to ping several machines, each one in a
    > separate thread and I'm seing strange things like when one machine is on
    > all three machines are reported on (although two are off). When I run my
    > program with one thread only the on/off state shows as expected.
    >
    > So I'm wondering if the Net::ping::ICMP#ping method is thread safe?
    > Daniel said he made no attempt to make sure that the code is and
    > usggested that I post this question here.
    >
    > The code for the method Net::ping::ICMP#ping is here:
    > http://www.koders.com/ruby/fidE6256BC790B8AD197544CE26287B5C7D3200E4C8.aspx?s=icmp#L5
    >
    >
    > (line 61)
    >
    > Here is also a simplified version of my own code:
    >
    > -----------------------------------------------
    >
    > require 'rubygems'
    > require 'net/ping/icmp'
    >
    > WORK_STATIONS = ['192.168.1.1', '192.168.1.2', '192.168.1.3']
    > threads = []
    > ping_objects = []
    >
    > WORK_STATIONS.each do |ip|
    > ping_objects << Net::ping::ICMP.new(ip)
    > threads << Thread.new(ip, ping_objects.last) do |ip, p|
    > puts "Monitoring #{ip}..."
    > loop do
    > puts "#{ip} is #{p.ping ? 'on' : 'off'}"
    > sleep 2
    > end
    > end
    > end
    >
    > threads.each { |th| th.join }


    Very late reply here.

    Odd. When I ran nearly identical code it worked as expected. However,
    when applied Joel's patch it didn't work right - it reported all hosts
    as off. Not sure why.

    That was on OS X 10.4.9 with Ruby 1.8.6-368 btw.

    Regards,

    Dan
    Daniel Berger, Aug 13, 2009
    #8
  9. Daniel Berger wrote:
    ...
    > Very late reply here.


    Hi, Daniel,

    Found the original and dusted it off...

    http://blade.nagaokaut.ac.jp/cgi-bin/vframe.rb/ruby/ruby-talk/294693?294583-296534

    > Odd. When I ran nearly identical code it worked as expected. However,
    > when applied Joel's patch it didn't work right - it reported all hosts
    > as off. Not sure why.


    Will take a look on linux first. (This is not code I've used, but I'm
    curious...)

    > That was on OS X 10.4.9 with Ruby 1.8.6-368 btw.


    I'll try there too.

    What were the target hosts? OS X, too?

    --
    vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407
    Joel VanderWerf, Aug 13, 2009
    #9
  10. On Aug 13, 2:41=A0pm, Joel VanderWerf <> wrote:
    > Daniel Berger wrote:
    >
    > ...
    >
    > > Very late reply here.

    >
    > Hi, Daniel,
    >
    > Found the original and dusted it off...
    >
    > http://blade.nagaokaut.ac.jp/cgi-bin/vframe.rb/ruby/ruby-talk/294693?...
    >
    > > Odd. When I ran nearly identical code it worked as expected. However,
    > > when applied Joel's patch it didn't work right - it reported all hosts
    > > as off. Not sure why.

    >
    > Will take a look on linux first. (This is not code I've used, but I'm
    > curious...)
    >
    > > That was on OS X 10.4.9 with Ruby 1.8.6-368 btw.

    >
    > I'll try there too.
    >
    > What were the target hosts? OS X, too?


    Yeah, I just ran it against 192.168.0.1 (exists), 192.168.0.2
    (exists), and 192.168.0.3 (does not exist).

    Regards,

    Dan
    Daniel Berger, Aug 13, 2009
    #10
  11. Laurent Julliard

    Leo Stern Guest

    Joel VanderWerf wrote:
    > Forgot to make the previous patch thread safe:
    >
    > --- net/ping/icmp.rb.bck 2008-03-15 13:19:42.000000000 -0700
    > +++ net/ping/icmp.rb 2008-03-15 13:19:42.000000000 -0700
    > @@ -1,5 +1,6 @@
    > $LOAD_PATH.unshift File.dirname(__FILE__)
    > require 'ping'
    > +require 'thread'
    >




    This does not work.
    --
    Posted via http://www.ruby-forum.com/.
    Leo Stern, Nov 15, 2009
    #11
  12. Laurent Julliard

    Leo Stern Guest

    >> dog = PingerController.new
    => #<PingerController:0x10232dc20>
    >> dog.pingHostThreaded

    Monitoring 173.45.228.218...Monitoring 173.45.235.145...Monitoring
    192.168.1.3...
    Monitoring 127.0.0.1...
    Monitoring 24.121.214.12...


    173.45.235.145 is off173.45.228.218 is off192.168.1.3 is off127.0.0.1 is
    off


    24.121.214.12 is off

    173.45.228.218 is off24.121.214.12 is off127.0.0.1 is off192.168.1.3 is
    off173.45.235.145 is off




    24.121.214.12 is off127.0.0.1 is off192.168.1.3 is off173.45.235.145 is
    off173.45.228.218 is off




    127.0.0.1 is off192.168.1.3 is off173.45.235.145 is off173.45.228.218 is
    off24.121.214.12 is off




    173.45.228.218 is off24.121.214.12 is off127.0.0.1 is off192.168.1.3 is
    off173.45.235.145 is off




    ^X^CIRB::Abort: abort then interrupt!!
    from /usr/local/lib/ruby/1.8/irb.rb:81:in `irb_abort'
    from /usr/local/lib/ruby/1.8/irb.rb:247:in `signal_handle'
    from /usr/local/lib/ruby/1.8/irb.rb:66:in `start'
    from
    /Users/lsternlicht/pinger/app/controllers/pinger_controller.rb:32:in
    `call'
    from
    /Users/lsternlicht/pinger/app/controllers/pinger_controller.rb:32:in
    `join'
    from
    /Users/lsternlicht/pinger/app/controllers/pinger_controller.rb:32:in
    `pingHostThreaded'
    from
    /Users/lsternlicht/pinger/app/controllers/pinger_controller.rb:32:in
    `each'
    from
    /Users/lsternlicht/pinger/app/controllers/pinger_controller.rb:32:in
    `pingHostThreaded'
    from (irb):2
    >> Net::ping::ICMP.new('173.45.235.145')

    => #<Net::ping::ICMP:0x102301148 @duration=nil, @timeout=5, @port=nil,
    @host="173.45.235.145", @bind_port=0, @warning=nil, @pid=26254,
    @exception=nil,
    @data="\000\001\002\003\004\005\006\a\b\t\n\v\f\r\016\017\020\021\022\023\024\025\026\027\030\031\032\e\034\035\036\037
    !\"\#$%&'()*+,-./012345678", @bind_host=nil, @data_size=56>
    >> Net::ping::ICMP.new('173.45.228.218')

    24.121.214.12 is off127.0.0.1 is off192.168.1.3 is off173.45.235.145 is
    off173.45.228.218 is off=> #<Net::ping::ICMP:0x1022c2f10 @duration=nil,
    @timeout=5, @port=nil, @host="173.45.228.218", @bind_port=0,
    @warning=nil, @pid=26254, @exception=nil,
    @data="\000\001\002\003\004\005\006\a\b\t\n\v\f\r\016\017\020\021\022\023\024\025\026\027\030\031\032\e\034\035\036\037
    !\"\#$%&'()*+,-./012345678", @bind_host=nil, @data_size=56>
    >>



    anyone have any ideas about the last object instantiation result?
    --
    Posted via http://www.ruby-forum.com/.
    Leo Stern, Nov 15, 2009
    #12
  13. Laurent Julliard

    Leo Stern Guest

    The gem is not thread-safe. I am using ruby 1.8 and Mac os X 10.6. I
    think Joel has the right idea but I got the same "off" result as Daniel.
    --
    Posted via http://www.ruby-forum.com/.
    Leo Stern, Nov 16, 2009
    #13
  14. Laurent Julliard

    Leo Stern Guest

    Jean-François Trân wrote:
    > 2008/3/16, Laurent Julliard <>:
    >
    >> Your recommendation is to turn the sequence number into a class
    >> variable so shouldn't the sequence number really be named @@seq
    >> rather than @seq if we want the sequence number to be unique across
    >> multiple instances of ICMP objects?

    >
    > If I am correct, Joel is using a ICMP class instance variable instead
    > of a class variable. So in the patch, @seq is not a var of an ICMP
    > object but an instance variable of ICMP class (as an object :) ).
    >
    > -- Jean-Fran�ois.


    Why?
    --
    Posted via http://www.ruby-forum.com/.
    Leo Stern, Dec 4, 2009
    #14
  15. 2009/12/4 Leo Stern <>:
    > Jean-Fran=E7ois Tr=E2n wrote:
    >> 2008/3/16, Laurent Julliard <>:
    >>
    >>> =A0Your recommendation is to turn the sequence number into a class
    >>> variable so shouldn't the sequence number really be named @@seq
    >>> rather than @seq if we want the sequence number to be unique across
    >>> multiple instances of ICMP objects?

    >>
    >> If I am correct, Joel is using a ICMP class instance variable instead
    >> of a class variable. So in the patch, @seq is not a var of an ICMP
    >> object but an instance variable of ICMP class (as an object :) ).

    >
    > Why?


    Because @@class variables are broken. You can find numerous threads
    about the topic here.

    Kind regards

    robert

    --=20
    remember.guy do |as, often| as.you_can - without end
    http://blog.rubybestpractices.com/
    Robert Klemme, Dec 4, 2009
    #15
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Francis Reed
    Replies:
    2
    Views:
    725
    Erik Funkenbusch
    Apr 8, 2006
  2. Irmen de Jong
    Replies:
    5
    Views:
    380
    Irmen de Jong
    Sep 28, 2004
  3. Gabriel Rossetti
    Replies:
    0
    Views:
    1,312
    Gabriel Rossetti
    Aug 29, 2008
  4. Ian Collins

    Re: How to Easily Make a Function Thread-Safe

    Ian Collins, Jan 12, 2009, in forum: C Programming
    Replies:
    4
    Views:
    882
    Phil Carmody
    Jan 13, 2009
  5. John Nagle
    Replies:
    5
    Views:
    466
    John Nagle
    Mar 12, 2012
Loading...

Share This Page