summaryrefslogtreecommitdiff
path: root/lib/gserver.rb
blob: f6f37d3a89534a1d89fd32f5560ee2632727a847 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#
# Copyright (C) 2001 John W. Small All Rights Reserved
#
# Author::        John W. Small
# Documentation:: Gavin Sinclair
# Licence::       Freeware.

require "socket"
require "thread"

#
# GServer implements a generic server, featuring thread pool management,
# simple logging, and multi-server management.  See HttpServer in
# <tt>xmlrpc/httpserver.rb</tt> in the Ruby standard library for an example of
# GServer in action.
#
# Any kind of application-level server can be implemented using this class.
# It accepts multiple simultaneous connections from clients, up to an optional
# maximum number.  Several _services_ (i.e. one service per TCP port) can be
# run simultaneously, and stopped at any time through the class method
# <tt>GServer.stop(port)</tt>.  All the threading issues are handled, saving
# you the effort.  All events are optionally logged, but you can provide your
# own event handlers if you wish.
#
# == Example
#
# Using GServer is simple.  Below we implement a simple time server, run it,
# query it, and shut it down.  Try this code in +irb+:
#
#   require 'gserver'
#
#   #
#   # A server that returns the time in seconds since 1970.
#   #
#   class TimeServer < GServer
#     def initialize(port=10001, *args)
#       super(port, *args)
#     end
#     def serve(io)
#       io.puts(Time.now.to_s)
#     end
#   end
#
#   # Run the server with logging enabled (it's a separate thread).
#   server = TimeServer.new
#   server.audit = true                  # Turn logging on.
#   server.start
#
#   # *** Now point your browser to http://localhost:10001 to see it working ***
#
#   # See if it's still running.
#   GServer.in_service?(10001)           # -> true
#   server.stopped?                      # -> false
#
#   # Shut the server down gracefully.
#   server.shutdown
#
#   # Alternatively, stop it immediately.
#   GServer.stop(10001)
#   # or, of course, "server.stop".
#
# All the business of accepting connections and exception handling is taken
# care of.  All we have to do is implement the method that actually serves the
# client.
#
# === Advanced
#
# As the example above shows, the way to use GServer is to subclass it to
# create a specific server, overriding the +serve+ method.  You can override
# other methods as well if you wish, perhaps to collect statistics, or emit
# more detailed logging.
#
# * #connecting
# * #disconnecting
# * #starting
# * #stopping
#
# The above methods are only called if auditing is enabled, via #audit=.
#
# You can also override #log and #error if, for example, you wish to use a
# more sophisticated logging system.
#
class GServer

  DEFAULT_HOST = "127.0.0.1"

  def serve(io)
  end

  @@services = {}   # Hash of opened ports, i.e. services
  @@servicesMutex = Mutex.new

  # Stop the server running on the given port, bound to the given host
  #
  # +port+:: port, as a FixNum, of the server to stop
  # +host+:: host on which to find the server to stop
  def GServer.stop(port, host = DEFAULT_HOST)
    @@servicesMutex.synchronize {
      @@services[host][port].stop
    }
  end

  # Check if a server is running on the given port and host
  #
  # +port+:: port, as a FixNum, of the server to check
  # +host+:: host on which to find the server to check
  #
  # Returns true if a server is running on that port and host.
  def GServer.in_service?(port, host = DEFAULT_HOST)
    @@services.has_key?(host) and
      @@services[host].has_key?(port)
  end

  # Stop the server
  def stop
    @connectionsMutex.synchronize  {
      if @tcpServerThread
        @tcpServerThread.raise "stop"
      end
    }
  end

  # Returns true if the server has stopped.
  def stopped?
    @tcpServerThread == nil
  end

  # Schedule a shutdown for the server
  def shutdown
    @shutdown = true
  end

  # Return the current number of connected clients
  def connections
    @connections.size
  end

  # Join with the server thread
  def join
    @tcpServerThread.join if @tcpServerThread
  end

  # Port on which to listen, as a FixNum
  attr_reader :port
  # Host on which to bind, as a String
  attr_reader :host
  # Maximum number of connections to accept at at ime, as a FixNum
  attr_reader :maxConnections
  # IO Device on which log messages should be written
  attr_accessor :stdlog
  # Set to true to cause the callbacks #connecting, #disconnecting, #starting,
  # and #stopping to be called during the server's lifecycle
  attr_accessor :audit
  # Set to true to show more detailed logging
  attr_accessor :debug

  # Called when a client connects, if auditing is enabled.
  #
  # +client+:: a TCPSocket instances representing the client that connected
  #
  # Return true to allow this client to connect, false to prevent it.
  def connecting(client)
    addr = client.peeraddr
    log("#{self.class.to_s} #{@host}:#{@port} client:#{addr[1]} " +
        "#{addr[2]}<#{addr[3]}> connect")
    true
  end


  # Called when a client disconnects, if audition is enabled.
  #
  # +clientPort+:: the port of the client that is connecting
  def disconnecting(clientPort)
    log("#{self.class.to_s} #{@host}:#{@port} " +
      "client:#{clientPort} disconnect")
  end

  protected :connecting, :disconnecting

  # Called when the server is starting up, if auditing is enabled.
  def starting()
    log("#{self.class.to_s} #{@host}:#{@port} start")
  end

  # Called when the server is shutting down, if auditing is enabled.
  def stopping()
    log("#{self.class.to_s} #{@host}:#{@port} stop")
  end

  protected :starting, :stopping

  # Called if #debug is true whenever an unhandled exception is raised.
  # This implementation simply logs the backtrace.
  #
  # +detail+:: The Exception that was caught
  def error(detail)
    log(detail.backtrace.join("\n"))
  end

  # Log a message to #stdlog, if it's defined.  This implementation
  # outputs the timestamp and message to the log.
  #
  # +msg+:: the message to log
  def log(msg)
    if @stdlog
      @stdlog.puts("[#{Time.new.ctime}] %s" % msg)
      @stdlog.flush
    end
  end

  protected :error, :log

  # Create a new server
  #
  # +port+:: the port, as a FixNum, on which to listen.
  # +host+:: the host to bind to
  # +maxConnections+:: The maximum number of simultaneous connections to
  #                    accept
  # +stdlog+:: IO device on which to log messages
  # +audit+:: if true, lifecycle callbacks will be called.  See #audit
  # +debug+:: if true, error messages are logged.  See #debug
  def initialize(port, host = DEFAULT_HOST, maxConnections = 4,
    stdlog = $stderr, audit = false, debug = false)
    @tcpServerThread = nil
    @port = port
    @host = host
    @maxConnections = maxConnections
    @connections = []
    @connectionsMutex = Mutex.new
    @connectionsCV = ConditionVariable.new
    @stdlog = stdlog
    @audit = audit
    @debug = debug
  end

  # Start the server if it isn't already running
  #
  # +maxConnections+::
  #   override +maxConnections+ given to the constructor.  A negative
  #   value indicates that the value from the constructor should be used.
  def start(maxConnections = -1)
    raise "server is already running" if !stopped?
    @shutdown = false
    @maxConnections = maxConnections if maxConnections > 0
    @@servicesMutex.synchronize  {
      if GServer.in_service?(@port,@host)
        raise "Port already in use: #{host}:#{@port}!"
      end
      @tcpServer = TCPServer.new(@host,@port)
      @port = @tcpServer.addr[1]
      @@services[@host] = {} unless @@services.has_key?(@host)
      @@services[@host][@port] = self;
    }
    @tcpServerThread = Thread.new {
      begin
        starting if @audit
        while !@shutdown
          @connectionsMutex.synchronize  {
             while @connections.size >= @maxConnections
               @connectionsCV.wait(@connectionsMutex)
             end
          }
          client = @tcpServer.accept
          @connections << Thread.new(client)  { |myClient|
            begin
              myPort = myClient.peeraddr[1]
              serve(myClient) if !@audit or connecting(myClient)
            rescue => detail
              error(detail) if @debug
            ensure
              begin
                myClient.close
              rescue
              end
              @connectionsMutex.synchronize {
                @connections.delete(Thread.current)
                @connectionsCV.signal
              }
              disconnecting(myPort) if @audit
            end
          }
        end
      rescue => detail
        error(detail) if @debug
      ensure
        begin
          @tcpServer.close
        rescue
        end
        if @shutdown
          @connectionsMutex.synchronize  {
             while @connections.size > 0
               @connectionsCV.wait(@connectionsMutex)
             end
          }
        else
          @connections.each { |c| c.raise "stop" }
        end
        @tcpServerThread = nil
        @@servicesMutex.synchronize  {
          @@services[@host].delete(@port)
        }
        stopping if @audit
      end
    }
    self
  end

end