Switch to threaded model to make the async model easier to work with

Also keeping request objects around and matching them to error
objects to ease debugging, and more debug output when
PUREX_DEBUG env var is set
This commit is contained in:
Vidar Hokstad 2024-01-11 19:22:43 +00:00
parent 0272848944
commit 5f3aca393e
3 changed files with 128 additions and 66 deletions

View file

@ -31,18 +31,24 @@ module X11
authorize(host, family, display_id) authorize(host, family, display_id)
@requestseq = 1 @requestseq = 1
@queue = [] @rqueue = Queue.new # Read but not returned events
@wqueue = Queue.new
@extensions = {} # Known extensions
@atoms = {} # Interned atoms
@extensions = {} start_io
# Interned atoms
@atoms = {}
end end
def event_handler= block def event_handler= block
@event_handler= block @event_handler= block
end end
def flush
while !@wqueue.empty?
sleep(0.01)
end
end
def display_info def display_info
@internal @internal
end end
@ -68,8 +74,15 @@ module X11
def read_error data def read_error data
error = Form::Error.from_packet(StringIO.new(data)) error = Form::Error.from_packet(StringIO.new(data))
# FIXME: Maybe make this configurable, as it means potentially
# keeping along really heavy requests. or alternative purge them
# more aggressively also when there are no errors, as otherwise
# the growth might be unbounded
error.request = @requests[error.sequence_number]
@requests.keys.find_all{|s| s <= error.sequence_number}.each do |s|
@requests.delete(s)
end
STDERR.puts "ERROR: #{error.inspect}" STDERR.puts "ERROR: #{error.inspect}"
raise error.inspect
error error
end end
@ -127,22 +140,24 @@ module X11
end end
def read_full_packet(len = 32) def read_full_packet(len = 32)
data = @socket.read_nonblock(32) data = @socket.read(32)
return nil if data.nil? return nil if data.nil?
while data.length < 32 while data.length < 32
IO.select([@socket],nil,nil,0.001) IO.select([@socket],nil,nil,0.001)
data.concat(@socket.read_nonblock(32 - data.length)) data.concat(@socket.read_nonblock(32 - data.length))
end end
return data return data
rescue IO::WaitReadable
return nil
end end
def read_packet timeout=5.0 def read_packet
IO.select([@socket],nil,nil, timeout)
data = read_full_packet(32) data = read_full_packet(32)
return nil if data.nil? return nil if data.nil?
# FIXME: Make it configurable.
@requests.keys.find_all{|s| s <= @requestseq - 50}.each do |s|
@requests.delete(s)
end
# FIXME: What is bit 8 for? Synthentic? # FIXME: What is bit 8 for? Synthentic?
type = data.unpack("C").first & 0x7f type = data.unpack("C").first & 0x7f
case type case type
@ -154,57 +169,100 @@ module X11
end end
end end
def write_raw_packet(pkt)
@requestseq += 1
@socket.write(pkt)
end
def write_packet(*args) def write_packet(*args)
pkt = args.join pkt = args.join
pkt[2..3] = u16(pkt.length/4) pkt[2..3] = u16(pkt.length/4)
write_raw_packet(pkt) @wqueue << [nil,nil,pkt]
end end
def write_request ob def write_request ob
data = ob.to_packet if ob.respond_to?(:to_packet) data = ob.to_packet(self) if ob.respond_to?(:to_packet)
raise "BAD LENGTH for #{ob.inspect} (#{ob.request_length.to_i*4} ! #{data.size} " if ob.request_length && ob.request_length.to_i*4 != data.size raise "BAD LENGTH for #{ob.inspect} (#{ob.request_length.to_i*4} ! #{data.size} " if ob.request_length && ob.request_length.to_i*4 != data.size
write_raw_packet(data) STDERR.puts "write_req: #{ob.inspect}" if @debug
@wqueue << [ob,nil,data]
end end
def write_sync(data, reply=nil) def write_sync(ob, reply=nil)
seq = @requestseq data = ob.to_packet(self) if ob.respond_to?(:to_packet)
write_request(data) q = Queue.new
pkt = next_reply(seq) @wqueue << [ob,q,data]
return nil if !pkt STDERR.puts "write_sync_req: #{ob.inspect}" if @debug
return pkt if pkt.is_a?(X11::Form::Error) pkt = q.shift
pp reply STDERR.puts "write_sync_rep: #{pkt.inspect}" if @debug
raise(pkt) if pkt.is_a?(X11::Form::Error)
return pkt if !pkt.is_a?(String)
reply ? reply.from_packet(StringIO.new(pkt)) : pkt reply ? reply.from_packet(StringIO.new(pkt)) : pkt
end end
def peek_packet def peek_packet = !@rqueue.empty?
!@queue.empty? def next_packet = @rqueue.shift
end
def next_packet def close = @rqueue.close
@queue.shift || read_packet
end
def next_reply(errseq) def start_io
# FIXME: This is totally broken @replies ||= {}
@requests ||= {}
# Read thread.
# FIXME: Drop the select.
rt = Thread.new do
while pkt = read_packet while pkt = read_packet
if pkt.is_a?(String) #STDERR.puts "read: #{pkt.inspect}"
return pkt if !pkt
elsif pkt.is_a?(X11::Form::Error) && pkt.sequence_number == errseq sleep 0.1
return pkt elsif pkt.is_a?(String)
else # This is a reply. We need the sequence number.
@queue.push(pkt) #
seq = pkt.unpack1("@2S")
STDERR.puts " - seq= #{seq}" if @debug
STDERR.puts @replies.inspect if @debug
if @replies[seq]
q = @replies.delete(seq)
STDERR.puts " - reply to #{q}" if @debug
q << pkt
end end
elsif pkt.is_a?(X11::Form::Error)
if @replies[pkt.sequence_number]
q = @replies.delete(pkt.sequence_number)
q << pkt
else
@rqueue << pkt
end
else
@rqueue << pkt
end
end
@rqueue.close
@replies.values.each(&:close)
end
# Write thread
wt = Thread.new do
while msg = @wqueue.shift
ob, q, data = *msg
@requests[@requestseq] = ob
@replies[@requestseq] = q if q
@requestseq = (@requestseq + 1) % 65536
@socket.write(data)
end
end
at_exit do
flush
@rqueue.close
@wqueue.close
# We kill this because it may be stuck in a read
# we'll never care about
Thread.kill(rt)
# We wait for this to finish because otherwise we may
# lose side-effects
wt.join
end end
end end
def run def run
loop do loop do
pkt = read_packet pkt = next_packet
return if !pkt return if !pkt
yield(pkt) yield(pkt)
end end
@ -215,13 +273,15 @@ module X11
d.depth == depth }.visuals.find{|v| v.qlass = qlass } d.depth == depth }.visuals.find{|v| v.qlass = qlass }
end end
def default_root = screens.first.root
# Requests # Requests
def create_window(x,y,w,h, def create_window(x,y,w,h,
values: {}, values: {},
depth: 32, parent: nil, border_width: 0, wclass: X11::Form::InputOutput, visual: nil depth: 32, parent: nil, border_width: 0, wclass: X11::Form::InputOutput, visual: nil
) )
wid = new_id wid = new_id
parent ||= screens.first.root parent ||= default_root
if visual.nil? if visual.nil?
visual = find_visual(0, depth).visual_id visual = find_visual(0, depth).visual_id
@ -571,7 +631,6 @@ module X11
auth_name = "" auth_name = ""
auth_data = "" auth_data = ""
end end
p [auth_name, auth_data]
handshake = Form::ClientHandshake.new( handshake = Form::ClientHandshake.new(
Protocol::BYTE_ORDER, Protocol::BYTE_ORDER,
@ -581,7 +640,7 @@ module X11
auth_data auth_data
) )
@socket.write(handshake.to_packet) @socket.write(handshake.to_packet(self))
data = @socket.read(1) data = @socket.read(1)
raise AuthorizationError, "Failed to read response from server" if !data raise AuthorizationError, "Failed to read response from server" if !data

View file

@ -44,7 +44,7 @@ module X11
end end
end end
def to_packet def to_packet(dpy)
# fetch class level instance variable holding defined fields # fetch class level instance variable holding defined fields
structs = self.class.structs structs = self.class.structs
@ -64,10 +64,10 @@ module X11
#p [s,value] #p [s,value]
if value.is_a?(BaseForm) if value.is_a?(BaseForm)
v = value.to_packet v = value.to_packet(dpy)
else else
#p [s,value] #p [s,value]
v = s.type_klass.pack(value) v = s.type_klass.pack(value, dpy)
end end
#p v #p v
v v
@ -77,15 +77,15 @@ module X11
when :length, :format_length when :length, :format_length
#p [s,value] #p [s,value]
#p [value.size] #p [value.size]
s.type_klass.pack(value.size) s.type_klass.pack(value.size, dpy)
when :string when :string
s.type_klass.pack(value) s.type_klass.pack(value, dpy)
when :list when :list
Array(value).collect do |obj| Array(value).collect do |obj|
if obj.is_a?(BaseForm) if obj.is_a?(BaseForm)
obj.to_packet obj.to_packet(dpy)
else else
s.type_klass.pack(obj) s.type_klass.pack(obj, dpy)
end end
end end
end end

View file

@ -11,7 +11,7 @@ module X11
def self.config(d,b) = (@directive, @bytesize = d,b) def self.config(d,b) = (@directive, @bytesize = d,b)
def self.pack(x) def self.pack(x, dpy)
if x.is_a?(Symbol) if x.is_a?(Symbol)
if (t = X11::Form.const_get(x)) && t.is_a?(Numeric) if (t = X11::Form.const_get(x)) && t.is_a?(Numeric)
x = t x = t
@ -27,7 +27,6 @@ module X11
def self.from_packet(sock) = unpack(sock.read(size)) def self.from_packet(sock) = unpack(sock.read(size))
end end
# Primitive Types
class Int8 < BaseType; config("c",1); end class Int8 < BaseType; config("c",1); end
class Int16 < BaseType; config("s",2); end class Int16 < BaseType; config("s",2); end
class Int32 < BaseType; config("l",4); end class Int32 < BaseType; config("l",4); end
@ -36,16 +35,14 @@ module X11
class Uint32 < BaseType; config("L",4); end class Uint32 < BaseType; config("L",4); end
class Message class Message
def self.pack(x) = x.b def self.pack(x,dpy) = x.b
def self.unpack(x) = x.b def self.unpack(x) = x.b
def self.size = 20 def self.size = 20
def self.from_packet(sock) = sock.read(2).b def self.from_packet(sock) = sock.read(2).b
end end
class String8 class String8
def self.pack(x) def self.pack(x, dpy) = (x.b + "\x00"*(-x.length & 3))
x.b + "\x00"*(-x.length & 3)
end
def self.unpack(socket, size) def self.unpack(socket, size)
raise "Expected size for String8" if size.nil? raise "Expected size for String8" if size.nil?
@ -57,7 +54,7 @@ module X11
end end
class String16 class String16
def self.pack(x) def self.pack(x, dpy)
x.encode("UTF-16BE").b + "\x00\x00"*(-x.length & 1) x.encode("UTF-16BE").b + "\x00\x00"*(-x.length & 1)
end end
@ -71,12 +68,12 @@ module X11
class String8Unpadded class String8Unpadded
def self.pack(x) = x def self.pack(x,dpy) = x
def self.unpack(socket, size) = socket.read(size) def self.unpack(socket, size) = socket.read(size)
end end
class Bool class Bool
def self.pack(x) = (x ? "\x01" : "\x00") def self.pack(x, dpy) = (x ? "\x01" : "\x00")
def self.unpack(str) = (str[0] == "\x01") def self.unpack(str) = (str[0] == "\x01")
def self.size = 1 def self.size = 1
end end
@ -96,10 +93,16 @@ module X11
Colormap = Uint32 Colormap = Uint32
Drawable = Uint32 Drawable = Uint32
Fontable = Uint32 Fontable = Uint32
Atom = Uint32
VisualID = Uint32 VisualID = Uint32
Mask = Uint32 Mask = Uint32
Timestamp = Uint32 Timestamp = Uint32
Keysym = Uint32 Keysym = Uint32
class Atom
def self.pack(x,dpy) = [dpy.atom(x)].pack("L")
def self.unpack(x) = x.nil? ? nil : x.unpack1("L")
def self.size = 4
def self.from_packet(sock) = unpack(sock.read(size))
end
end end
end end