Server

class
Superclass
Object
Extended With
Loggability

A mercurial server object. This uses the Mercurial Command Server protocol to execute Mercurial commands.

Refs:

Constants

COMMAND_TEMPLATE

Array#pack template for commands sent to the command server

EXTENSION_DISABLED_DETAILS

A Regexp to match the detail message when a command belongs to a disabled extension.

HEADER_TEMPLATE

String#unpack template for message headers from the command server

MESSAGE_TEMPLATE

Array#pack template for plain messages sent to the command server

Attributes

args[R]

The additional arguments to send to the command server on startup

byte_input_callback[RW]

The callable used to fetch byte-oriented input

line_input_callback[RW]

The callable used to fetch line-oriented input

pid[RW]

The PID of the running command server if there is one

reader[RW]

The reader end of the pipe used to communicate with the command server.

repo[R]

The Pathname to the repository the server should target

writer[RW]

The writer end of the pipe used to communicate with the command server.

Public Class Methods

anchor
make_command_option( optname, value )

Form one or more command line options given an optname and value and return them as an Array.

# File lib/hglib/server.rb, line 54
def self::make_command_option( optname, value )
        case value
        when TrueClass
                return [ optname ]
        when FalseClass, NilClass
                return [ optname.sub(/\A--/, '--no-') ] if optname.start_with?( '--' )
        when String, Numeric
                if optname.start_with?( '--' )
                        return [ "#{optname}=#{value}" ]
                else
                        return [ optname, value ]
                end
        when Array
                return value.map {|v| self.make_command_option(optname, v) }
        else
                raise ArgumentError, "can't handle command option: %p" % [{ name => value }]
        end
end
anchor
mangle_options( **options )

Turn the specified opthash into an Array of command line options.

# File lib/hglib/server.rb, line 42
def self::mangle_options( **options )
        return options.flat_map do |name, val|
                prefix = name.length > 1 ? '--' : '-'
                optname = "%s%s" % [ prefix, name.to_s.gsub(/_/, '-') ]

                self.make_command_option( optname, val )
        end.compact
end
anchor
new( repo=nil, **args )

Create a new Hglib::Server that will be invoked for the specified repo. Any additional args given will be passed to the `hg serve` command on startup.

# File lib/hglib/server.rb, line 77
def initialize( repo=nil, **args )
        @repo = Pathname( repo ) if repo

        @reader = nil
        @writer = nil

        @pid = nil

        @byte_input_callback = nil
        @line_input_callback = nil
end

Public Instance Methods

anchor
handle_errors( command, errors, details )

Form and raise an exception for the given errors resulting from running command.

# File lib/hglib/server.rb, line 203
def handle_errors( command, errors, details )
        err = nil

        if details && (m = details.match(EXTENSION_DISABLED_DETAILS) )
                err = Hglib::DisabledExtensionError.new( command, m[:extension_name] )
        else
                err = Hglib::CommandError.new( command, errors, details: details )
        end

        raise( err, nil, caller(2) )
end
anchor
is_started?()

Returns true if the underlying command server has been started.

# File lib/hglib/server.rb, line 228
def is_started?
        return self.pid ? true : false
end
Also aliased as: started?
anchor
on_byte_input( &callback )

Register a callback that will be called when the command server asks for byte-oriented input. The callback will be called with the (maximum) number of bytes to return.

# File lib/hglib/server.rb, line 128
def on_byte_input( &callback )
        raise LocalJumpError, "no block given" unless callback
        self.byte_input_callback = callback
end
anchor
on_line_input( &callback )

Register a callback that will be called when the command server asks for line-oriented input. The callback will be called with the (maximum) number of bytes to return.

# File lib/hglib/server.rb, line 137
def on_line_input( &callback )
        raise LocalJumpError, "no block given" unless callback
        self.line_input_callback = callback
end
anchor
register_input_callbacks( io=$stdin )

Register the callbacks necessary to read both line and byte input from the specified io, which is expected to respond to gets and read.

# File lib/hglib/server.rb, line 145
def register_input_callbacks( io=$stdin )
        self.on_byte_input( &io.method(:read) )
        self.on_line_input( &io.method(:gets) )
end
anchor
run( command, *args, **options )

Run the specified command with the given args via the server and return the result. If the command requires input, the callbacks registered with on_byte_input and on_line_input will be used to read it. If one of these callbacks is not registered, an IOError will be raised.

# File lib/hglib/server.rb, line 155
def run( command, *args, **options )
        args = args.compact
        self.log.debug { "Running command: %p" % [ Shellwords.join([command.to_s] + args) ] }
        self.start unless self.started?

        done = false
        output = String.new
        errors = []

        args += self.class.mangle_options( **options )
        self.write_command( 'runcommand', command, *args )

        until done
                channel, data = self.read_message

                case channel
                when 'o'
                        # self.log.debug "Got command output: %p" % [ data ]
                        output << data
                when 'r'
                        done = true
                when 'e'
                        self.log.debug "Got command error: %p" % [ data ]
                        errors << data
                when 'L'
                        self.log.debug "Server requested line input (%d bytes)" % [ data ]
                        input = self.get_line_input( data.to_i )
                        self.write_message( input.chomp + "\n" )
                when 'I'
                        self.log.debug "Server requested byte input (%d bytes)" % [ data ]
                        input = self.get_byte_input( data.to_i )
                        self.write_message( input )
                else
                        msg = "Unexpected channel %p" % [ channel ]
                        self.log.error( msg )
                        raise( msg ) if channel =~ /\p{Upper}/ # Mandatory
                end
        end

        self.handle_errors( command, errors, output ) unless errors.empty?

        self.log.debug { "Got %s response: %p" % [ command.to_s.upcase, output ] }
        return output
end
anchor
run_with_json_template( command, *args, symbolize: true, **options )

Run the specified command with the given args with the JSON template and return the result.

# File lib/hglib/server.rb, line 218
def run_with_json_template( command, *args, symbolize: true, **options )
        options[:T] = 'json'

        json = self.run( command, *args, **options )

        return JSON.parse( json, symbolize_names: symbolize )
end
anchor
start()

Open a pipe and start the command server.

# File lib/hglib/server.rb, line 235
def start
        self.log.debug "Starting."
        self.spawn_server
        self.read_hello
end
anchor
started?()
Alias for: is_started?
anchor
stop()

Stop the command server and clean up the pipes.

# File lib/hglib/server.rb, line 243
def stop
        return unless self.started?

        self.log.debug "Stopping."
        self.writer.close if self.writer
        self.writer = nil
        self.reader.close if self.reader
        self.reader = nil
        self.stop_server
end

Protected Instance Methods

anchor
get_byte_input( max_bytes )

Call the on_byte_input callback to read at most max_bytes. Raises an IOError if no callback is registered.

# File lib/hglib/server.rb, line 271
def get_byte_input( max_bytes )
        callback = self.byte_input_callback or
                raise IOError, "cannot read input: no byte input callback registered"

        return callback.call( max_bytes )
end
anchor
get_line_input( max_bytes )

Call the on_line_input callback to read at most max_bytes. Raises an IOError if no callback is registered.

# File lib/hglib/server.rb, line 261
def get_line_input( max_bytes )
        callback = self.line_input_callback or
                raise IOError, "cannot read input: no line input callback registered"

        return callback.call( max_bytes )
end
anchor
read_hello()

Read the cmdserver's banner.

# File lib/hglib/server.rb, line 323
def read_hello
        _, message = self.read_message
        self.log.debug "Hello message:\n%s" % [ message ]
end
anchor
read_message()

Read a single channel identifier and message from the command server. Raises an exception if the server is not yet started.

# File lib/hglib/server.rb, line 331
def read_message
        raise "Server is not yet started" unless self.started?
        header = self.reader.read( 5 ) or raise "Server aborted."
        channel, bytes = header.unpack( HEADER_TEMPLATE )
        self.log.debug "Read channel %p message (%d bytes)" % [ channel, bytes ]

        # Input requested; return the requested length as the message
        if channel == 'I' || channel == 'L'
                return channel, bytes
        end

        self.log.debug "Reading %d more bytes of the message" % [ bytes ]
        message = self.reader.read( bytes ) unless bytes.zero?
        self.log.debug { "  read message: %p" % [ message ] }
        return channel, message
end
anchor
server_start_command()

Return the command-line command for starting the command server.

# File lib/hglib/server.rb, line 350
def server_start_command
        hg = Hglib.hg_path
        raise "couldn't find an `hg' executable in your PATH!" unless hg.executable?

        cmd = [
                hg.to_s,
                '--config',
                'ui.interactive=True',
                'serve',
                '--cmdserver',
                'pipe',
        ]

        cmd << '--repository' << self.repo.to_s if self.repo

        return cmd
end
anchor
spawn_server()

Fork a child and run Mercurial in command-server mode.

# File lib/hglib/server.rb, line 280
def spawn_server
        self.reader, child_writer = IO.pipe
        child_reader, self.writer = IO.pipe

        cmd = self.server_start_command
        self.pid = Process.spawn( *cmd, out: child_writer, in: child_reader, close_others: true )
        self.log.debug "Spawned command server at PID %d" % [ self.pid ]

        child_writer.close
        child_reader.close
end
anchor
stop_server()

Kill the command server if it's running

# File lib/hglib/server.rb, line 294
def stop_server
        if self.pid
                self.log.debug "Stopping command server at PID %d" % [ self.pid ]
                Process.kill( :TERM, self.pid )
                Process.wait( self.pid, Process::WNOHANG )
                self.pid = nil
        end
end
anchor
write_command( command, *args )

Write the specified message to the command server. Raises an exception if the server is not yet started.

# File lib/hglib/server.rb, line 306
def write_command( command, *args )
        data = args.map( &:to_s ).join( "\0" )
        message = [ command + "\n", data.bytesize, data ].pack( COMMAND_TEMPLATE )
        self.log.debug "Writing command %p to command server." % [ message ]
        self.writer.write( message )
end
anchor
write_message( data )

Write the specified message to the command server.

# File lib/hglib/server.rb, line 315
def write_message( data )
        message = [ data.bytesize, data ].pack( MESSAGE_TEMPLATE )
        self.log.debug "Writing message %p to command server." % [ message ]
        self.writer.write( message )
end