mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-18 22:26:12 +01:00
Add working Lua gRPC client
This commit is contained in:
parent
b1109b57a7
commit
3b88b3ff11
7 changed files with 285 additions and 0 deletions
2
api/lua_grpc/.stylua.toml
Normal file
2
api/lua_grpc/.stylua.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
indent_type = "Spaces"
|
||||
column_width = 120
|
45
api/lua_grpc/pinnacle.lua
Normal file
45
api/lua_grpc/pinnacle.lua
Normal 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
|
135
api/lua_grpc/pinnacle/grpc/client.lua
Normal file
135
api/lua_grpc/pinnacle/grpc/client.lua
Normal 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
|
35
api/lua_grpc/pinnacle/grpc/protobuf.lua
Normal file
35
api/lua_grpc/pinnacle/grpc/protobuf.lua
Normal 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
|
51
api/lua_grpc/pinnacle/input.lua
Normal file
51
api/lua_grpc/pinnacle/input.lua
Normal 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
|
7
api/lua_grpc/pinnacle/loop.lua
Normal file
7
api/lua_grpc/pinnacle/loop.lua
Normal file
|
@ -0,0 +1,7 @@
|
|||
local cqueues = require("cqueues")
|
||||
|
||||
local loop = {
|
||||
loop = cqueues.new(),
|
||||
}
|
||||
|
||||
return loop
|
10
api/lua_grpc/test.lua
Normal file
10
api/lua_grpc/test.lua
Normal 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)
|
Loading…
Reference in a new issue