A mercurial server object. This uses the Mercurial Command Server
protocol to execute Mercurial commands.
Refs:
Array#pack template for commands sent to the command server
A Regexp to match the detail message when a command belongs to a disabled extension.
String#unpack template for message headers from the command server
Array#pack template for plain messages sent to the command server
The additional arguments to send to the command server on startup
The callable used to fetch byte-oriented input
The callable used to fetch line-oriented input
The PID of the running command server if there is one
The reader end of the pipe used to communicate with the command server.
The Pathname to the repository the server should target
The writer end of the pipe used to communicate with the command server.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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