// 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 . 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 }