Add working Lua gRPC client

This commit is contained in:
Ottatop 2024-01-11 21:58:35 -06:00
parent b1109b57a7
commit 3b88b3ff11
7 changed files with 285 additions and 0 deletions

View file

@ -0,0 +1,2 @@
indent_type = "Spaces"
column_width = 120

45
api/lua_grpc/pinnacle.lua Normal file
View file

@ -0,0 +1,45 @@
local cqueues = require("cqueues")
---@type ClientModule
local client = require("pinnacle.grpc.client")
---@class PinnacleModule
local pinnacle = {}
---@class Pinnacle
---@field private config_client Client
---@field private loop CqueuesLoop
---@field input Input
local Pinnacle = {}
function Pinnacle:quit()
self.config_client:unary_request({
service = "pinnacle.v0alpha1.PinnacleService",
method = "Quit",
request_type = "pinnacle.v0alpha1.QuitRequest",
data = {},
})
end
---Setup Pinnacle.
---@param config_fn fun(pinnacle: Pinnacle)
function pinnacle.setup(config_fn)
require("pinnacle.grpc.protobuf").build_protos()
local loop = cqueues.new()
---@type Client
local config_client = client.new(loop)
---@type Pinnacle
local self = {
config_client = config_client,
loop = loop,
input = require("pinnacle.input").new(config_client),
}
setmetatable(self, { __index = Pinnacle })
config_fn(self)
self.loop:loop()
end
return pinnacle

View file

@ -0,0 +1,135 @@
local socket = require("cqueues.socket")
local headers = require("http.headers")
local h2_connection = require("http.h2_connection")
local pb = require("pb")
local inspect = require("inspect")
---Create appropriate headers for a gRPC request.
---@param service string The desired service
---@param method string The desired method within the service
---@return HttpHeaders
local function create_request_headers(service, method)
local req_headers = headers.new()
req_headers:append(":method", "POST")
req_headers:append(":scheme", "http")
req_headers:append(":path", "/" .. service .. "/" .. method)
req_headers:append("te", "trailers")
req_headers:append("content-type", "application/grpc")
return req_headers
end
---@class ClientModule
local client = {}
---@class Client
---@field conn H2Connection
---@field loop CqueuesLoop
local Client = {}
---@return H2Stream stream An http2 stream
function Client:new_stream()
return self.conn:new_stream()
end
---@class GrpcRequestParams
---@field service string
---@field method string
---@field request_type string
---@field response_type string?
---@field data table
---Send a synchronous unary request to the compositor.
---
---If `response_type` is not specified then it will default to
---`google.protobuf.Empty`.
---@param grpc_request_params GrpcRequestParams
---@return table
function Client:unary_request(grpc_request_params)
local stream = self.conn:new_stream()
local service = grpc_request_params.service
local method = grpc_request_params.method
local request_type = grpc_request_params.request_type
local response_type = grpc_request_params.response_type or "google.protobuf.Empty"
local data = grpc_request_params.data
local encoded_protobuf = assert(pb.encode(request_type, data), "wrong table schema")
local packed_prefix = string.pack("I1", 0)
local payload_len = string.pack(">I4", encoded_protobuf:len())
local body = packed_prefix .. payload_len .. encoded_protobuf
stream:write_headers(create_request_headers(service, method), false)
stream:write_chunk(body, true)
local response_headers = stream:get_headers()
-- TODO: check headers for errors
local response_body = stream:get_next_chunk()
local response = pb.decode(response_type, response_body)
print(inspect(response))
return response
end
---Send a async server streaming request to the compositor.
---
---`callback` will be called with every streamed response.
---
---If `response_type` is not specified then it will default to
---`google.protobuf.Empty`.
---@param grpc_request_params GrpcRequestParams
---@param callback fun(response: table)
function Client:server_streaming_request(grpc_request_params, callback)
local stream = self.conn:new_stream()
local service = grpc_request_params.service
local method = grpc_request_params.method
local request_type = grpc_request_params.request_type
local response_type = grpc_request_params.response_type or "google.protobuf.Empty"
local data = grpc_request_params.data
local encoded_protobuf = assert(pb.encode(request_type, data), "wrong table schema")
local packed_prefix = string.pack("I1", 0)
local payload_len = string.pack(">I4", encoded_protobuf:len())
local body = packed_prefix .. payload_len .. encoded_protobuf
stream:write_headers(create_request_headers(service, method), false)
stream:write_chunk(body, true)
local response_headers = stream:get_headers()
-- TODO: check headers for errors
self.loop:wrap(function()
for response_body in stream:each_chunk() do
local response = pb.decode(response_type, response_body)
callback(response)
end
end)
end
---@return Client
function client.new(loop)
local sock = socket.connect({
host = "127.0.0.1",
port = "8080",
})
sock:connect()
local conn = h2_connection.new(sock, "client")
conn:connect()
---@type Client
local self = {
conn = conn,
loop = loop,
}
setmetatable(self, { __index = Client })
return self
end
return client

View file

@ -0,0 +1,35 @@
local pb = require("pb")
local protobuf = {}
function protobuf.build_protos()
local version = "v0alpha1"
local proto_file_paths = {
"/home/jason/projects/pinnacle/api/protocol/pinnacle/tag/" .. version .. "/tag.proto",
"/home/jason/projects/pinnacle/api/protocol/pinnacle/input/" .. version .. "/input.proto",
"/home/jason/projects/pinnacle/api/protocol/pinnacle/input/libinput/" .. version .. "/libinput.proto",
"/home/jason/projects/pinnacle/api/protocol/pinnacle/" .. version .. "/pinnacle.proto",
"/home/jason/projects/pinnacle/api/protocol/pinnacle/output/" .. version .. "/output.proto",
"/home/jason/projects/pinnacle/api/protocol/pinnacle/process/" .. version .. "/process.proto",
"/home/jason/projects/pinnacle/api/protocol/pinnacle/window/" .. version .. "/window.proto",
"/home/jason/projects/pinnacle/api/protocol/pinnacle/window/rules/" .. version .. "/rules.proto",
}
local cmd = "protoc --descriptor_set_out=/tmp/pinnacle.pb --proto_path=/home/jason/projects/pinnacle/api/protocol/ "
for _, file_path in pairs(proto_file_paths) do
cmd = cmd .. file_path .. " "
end
local proc = assert(io.popen(cmd), "protoc is not installed")
local _ = proc:read("a")
proc:close()
local pinnacle_pb = assert(io.open("/tmp/pinnacle.pb", "r"), "no pb file generated")
local pinnacle_pb_data = pinnacle_pb:read("a")
pinnacle_pb:close()
assert(pb.load(pinnacle_pb_data), "failed to load .pb file")
end
return protobuf

View file

@ -0,0 +1,51 @@
---@class InputModule
local input = {}
---@class Input
---@field private config_client Client
local Input = {}
---@enum Modifier
local modifier = {
SHIFT = 1,
CTRL = 2,
ALT = 3,
SUPER = 4,
}
---@param mods Modifier[]
---@param key integer | string
---@param action fun()
function Input:set_keybind(mods, key, action)
local raw_code = nil
local xkb_name = nil
if type(key) == "number" then
raw_code = key
elseif type(key) == "string" then
xkb_name = key
end
self.config_client:server_streaming_request({
service = "pinnacle.input.v0alpha1.InputService",
method = "SetKeybind",
request_type = "pinnacle.input.v0alpha1.SetKeybindRequest",
data = {
modifiers = mods,
-- oneof not violated because `key` can't be both an int and string
raw_code = raw_code,
xkb_name = xkb_name,
},
}, action)
end
function input.new(config_client)
---@type Input
local self = {
config_client = config_client,
}
setmetatable(self, { __index = Input })
return self
end
return input

View file

@ -0,0 +1,7 @@
local cqueues = require("cqueues")
local loop = {
loop = cqueues.new(),
}
return loop

10
api/lua_grpc/test.lua Normal file
View file

@ -0,0 +1,10 @@
local pinnacle = require("pinnacle")
pinnacle.setup(function(pinnacle)
pinnacle.input:set_keybind({ 1 }, "A", function()
print("hi from grpc keybind")
end)
pinnacle.input:set_keybind({ 1 }, "Q", function()
pinnacle:quit()
end)
end)