Files
qga/client.v
2025-06-10 00:04:11 +03:00

163 lines
4.3 KiB
V

// This file is part of qga.
//
// qga is free software: you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the
// Free Software Foundation, either version 3 of the License, or (at your
// option) any later version.
//
// qga is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
// License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with qga. If not, see <https://www.gnu.org/licenses/>.
module qga
import json
import net.unix
import time
// Client holds a connection to the QEMU Guest Agent socket.
struct Client {
mut:
stream unix.StreamConn
read_buffer_size int
}
// Client.new creates new Client instance connected to provided UNIX domain socket address.
// Example:
// ```
// mut ga := qga.Client.new('/tmp/qga0.sock')!
// ga.ping()!
// ```
pub fn Client.new(addr string, params ClientParams) !Client {
mut stream := unix.connect_stream(addr) or {
return GuestAgentError{
msg: 'unable to connect to socket at ${addr}: ${err}'
code: err_no_connect
err: err
is_unreachable: true
}
}
stream.set_write_timeout(params.timeout)
stream.set_read_timeout(params.timeout)
return Client{stream, params.read_buffer_size}
}
// Client.from_handle creates new Client instance connected to socket handle.
pub fn Client.from_handle(handle int, params ClientParams) !Client {
mut sock := unix.stream_socket_from_handle(handle) or {
return GuestAgentError{
msg: 'unable to connect to socket at ${handle}: ${err}'
code: err_no_connect
err: err
is_unreachable: true
}
}
mut stream := unix.StreamConn{
sock: sock
}
stream.set_write_timeout(params.timeout)
stream.set_read_timeout(params.timeout)
return Client{stream, params.read_buffer_size}
}
@[params]
pub struct ClientParams {
timeout time.Duration = 5 * time.second // socket write and read timeout
read_buffer_size int = 4096 // read buffer size in bytes
}
// Request represents a common QEMU Guest Agent Protocol request schema.
struct Request[T] {
execute string
arguments ?T
}
// Response represents a common QEMU Guest Agent Protocol response schema.
struct Response[T] {
return T
error ?struct {
class string
desc string
}
}
// execute is generic function to run arbitrary QGA commands.
fn (mut c Client) execute[T, R](name string, args ?T, params ExecuteParams) !Response[R] {
request := Request[T]{
execute: name
arguments: args
}
encoded := json.encode(request)
c.stream.wait_for_write() or {
return GuestAgentError{
msg: 'socket write timeout reached'
code: err_timed_out
err: err
is_unreachable: true
}
}
c.stream.write(encoded.bytes()) or {
return GuestAgentError{
msg: 'cannot write data to socket at ${c.stream.sock.handle}: ${err}'
code: err_cannot_write
err: err
is_unreachable: true
}
}
$if trace_guest_agent ? {
eprintln('>>> ${@MOD}.${@METHOD} Sent: ${encoded}')
}
if params.no_return {
$if trace_guest_agent ? {
eprintln('<<< ${@MOD}.${@METHOD} command ${name} is no return, skip reading response')
}
return Response[R]{}
}
mut buf := []u8{len: c.read_buffer_size}
c.stream.wait_for_read() or {
return GuestAgentError{
msg: 'socket read timeout reached'
code: err_timed_out
err: err
is_unreachable: true
}
}
c.stream.read(mut buf) or {
return GuestAgentError{
msg: 'cannot read from socket at ${c.stream.sock.handle}: ${err}'
code: err_cannot_read
err: err
is_unreachable: true
}
}
// trim trailing null bytes
mut idx := buf.len - 1
for idx >= 0 && buf[idx] == 0 {
idx--
}
str := buf[..idx + 1].bytestr().trim_space()
$if trace_guest_agent ? {
eprintln('<<< ${@MOD}.${@METHOD} Recv: ${str}')
}
decoded := json.decode(Response[R], str)!
if decoded.error != none {
return GuestAgentError{
msg: 'guest agent command failed'
code: err_from_agent
class: decoded.error.class
desc: decoded.error.desc
}
}
return decoded
}
@[params]
struct ExecuteParams {
pub:
no_return bool
}