538 lines
15 KiB
C
538 lines
15 KiB
C
/* $OpenBSD: vm_agentx.c,v 1.1 2022/09/13 10:28:19 martijn Exp $ */
|
|
|
|
/*
|
|
* Copyright (c) 2022 Martijn van Duren <martijn@openbsd.org>
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/sysctl.h>
|
|
#include <sys/un.h>
|
|
|
|
#include <agentx.h>
|
|
#include <errno.h>
|
|
#include <grp.h>
|
|
#include <pwd.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include "proc.h"
|
|
#include "vmd.h"
|
|
|
|
struct conn {
|
|
struct event ev;
|
|
struct agentx *agentx;
|
|
};
|
|
|
|
void vm_agentx_run(struct privsep *, struct privsep_proc *, void *);
|
|
int vm_agentx_dispatch_parent(int, struct privsep_proc *, struct imsg *);
|
|
void vm_agentx_configure(struct vmd_agentx *);
|
|
static void vm_agentx_nofd(struct agentx *, void *, int);
|
|
static void vm_agentx_tryconnect(int, short, void *);
|
|
static void vm_agentx_read(int, short, void *);
|
|
static void vm_agentx_flush_pending(void);
|
|
static int vm_agentx_sortvir(const void *, const void *);
|
|
static int vm_agentx_adminstate(int);
|
|
static int vm_agentx_operstate(int);
|
|
static void vm_agentx_vmHvSoftware(struct agentx_varbind *);
|
|
static void vm_agentx_vmHvVersion(struct agentx_varbind *);
|
|
static void vm_agentx_vmHvObjectID(struct agentx_varbind *);
|
|
static void vm_agentx_vminfo(struct agentx_varbind *);
|
|
|
|
static struct agentx_index *vmIndex;
|
|
static struct agentx_object *vmNumber, *vmName, *vmUUID, *vmOSType;
|
|
static struct agentx_object *vmAdminState, *vmOperState, *vmAutoStart;
|
|
static struct agentx_object *vmPersistent, *vmCurCpuNumber, *vmMinCpuNumber;
|
|
static struct agentx_object *vmMaxCpuNumber, *vmMemUnit, *vmCurMem, *vmMinMem;
|
|
static struct agentx_object *vmMaxMem;
|
|
|
|
static struct vmd_agentx *vmd_agentx;
|
|
|
|
static struct agentx_varbind **vminfo = NULL;
|
|
static size_t vminfolen = 0;
|
|
static size_t vminfosize = 0;
|
|
static int vmcollecting = 0;
|
|
|
|
#define VMMIB AGENTX_MIB2, 236
|
|
#define VMOBJECTS VMMIB, 1
|
|
#define VMHVSOFTWARE VMOBJECTS, 1, 1
|
|
#define VMHVVERSION VMOBJECTS, 1, 2
|
|
#define VMHVOBJECTID VMOBJECTS, 1, 3
|
|
#define VMNUMBER VMOBJECTS, 2
|
|
#define VMENTRY VMOBJECTS, 4, 1
|
|
#define VMINDEX VMENTRY, 1
|
|
#define VMNAME VMENTRY, 2
|
|
#define VMUUID VMENTRY, 3
|
|
#define VMOSTYPE VMENTRY, 4
|
|
#define VMADMINSTATE VMENTRY, 5
|
|
#define VMOPERSTATE VMENTRY, 6
|
|
#define VMAUTOSTART VMENTRY, 7
|
|
#define VMPERSISTENT VMENTRY, 8
|
|
#define VMCURCPUNUMBER VMENTRY, 9
|
|
#define VMMINCPUNUMBER VMENTRY, 10
|
|
#define VMMAXCPUNUMBER VMENTRY, 11
|
|
#define VMMEMUNIT VMENTRY, 12
|
|
#define VMCURMEM VMENTRY, 13
|
|
#define VMMINMEM VMENTRY, 14
|
|
#define VMMAXMEM VMENTRY, 15
|
|
|
|
#define AGENTX_GROUP "_agentx"
|
|
#define MEM_SCALE (1024 * 1024)
|
|
|
|
static struct privsep_proc procs[] = {
|
|
{ "parent", PROC_PARENT, vm_agentx_dispatch_parent },
|
|
};
|
|
|
|
void
|
|
vm_agentx(struct privsep *ps, struct privsep_proc *p)
|
|
{
|
|
struct group *grp;
|
|
|
|
/*
|
|
* Make sure we can connect to /var/agentx/master with the correct
|
|
* group permissions.
|
|
*/
|
|
if ((grp = getgrnam(AGENTX_GROUP)) == NULL)
|
|
fatal("getgrnam");
|
|
ps->ps_pw->pw_gid = grp->gr_gid;
|
|
|
|
proc_run(ps, p, procs, nitems(procs), vm_agentx_run, NULL);
|
|
}
|
|
|
|
void
|
|
vm_agentx_shutdown(void)
|
|
{
|
|
}
|
|
|
|
void
|
|
vm_agentx_run(struct privsep *ps, struct privsep_proc *p, void *arg)
|
|
{
|
|
/*
|
|
* pledge in agentx process
|
|
* stdio - for malloc and basic I/O including events.
|
|
* recvfd - for the proc fd exchange.
|
|
* unix - for access to the agentx master socket.
|
|
*/
|
|
if (pledge("stdio recvfd unix", NULL) == -1)
|
|
fatal("pledge");
|
|
}
|
|
|
|
int
|
|
vm_agentx_dispatch_parent(int fd, struct privsep_proc *p, struct imsg *imsg)
|
|
{
|
|
static struct vmop_info_result *vir = NULL;
|
|
static struct vmop_info_result *mvir = NULL;
|
|
struct vmd *env = p->p_ps->ps_env;
|
|
struct vmop_info_result *tvir;
|
|
struct agentx_object *reqObject;
|
|
static size_t nvir = 0;
|
|
static size_t virlen = 0;
|
|
static int error = 0;
|
|
size_t i, j, index;
|
|
enum agentx_request_type rtype;
|
|
|
|
switch (imsg->hdr.type) {
|
|
case IMSG_VMDOP_GET_INFO_VM_DATA:
|
|
if (error)
|
|
break;
|
|
if (nvir + 1 > virlen) {
|
|
tvir = reallocarray(vir, virlen + 10, sizeof(*vir));
|
|
if (tvir == NULL) {
|
|
log_warn("%s: Couldn't dispatch vm information",
|
|
__func__);
|
|
error = 1;
|
|
break;
|
|
}
|
|
virlen += 10;
|
|
vir = tvir;
|
|
}
|
|
memcpy(&(vir[nvir++]), imsg->data, sizeof(vir[nvir]));
|
|
break;
|
|
case IMSG_VMDOP_GET_INFO_VM_END_DATA:
|
|
vmcollecting = 0;
|
|
if (error) {
|
|
for (i = 0; i < vminfolen; i++)
|
|
agentx_varbind_error(vminfo[i]);
|
|
vminfolen = 0;
|
|
error = 0;
|
|
nvir = 0;
|
|
return (0);
|
|
}
|
|
|
|
qsort(vir, nvir, sizeof(*vir), vm_agentx_sortvir);
|
|
for (i = 0; i < vminfolen; i++) {
|
|
reqObject = agentx_varbind_get_object(vminfo[i]);
|
|
if (reqObject == vmNumber) {
|
|
agentx_varbind_integer(vminfo[i],
|
|
(int32_t)nvir);
|
|
continue;
|
|
}
|
|
index = agentx_varbind_get_index_integer(vminfo[i],
|
|
vmIndex);
|
|
rtype = agentx_varbind_request(vminfo[i]);
|
|
for (j = 0; j < nvir; j++) {
|
|
if (vir[j].vir_info.vir_id < index)
|
|
continue;
|
|
if (vir[j].vir_info.vir_id == index &&
|
|
(rtype == AGENTX_REQUEST_TYPE_GET ||
|
|
rtype ==
|
|
AGENTX_REQUEST_TYPE_GETNEXTINCLUSIVE))
|
|
break;
|
|
if (vir[j].vir_info.vir_id > index &&
|
|
(rtype == AGENTX_REQUEST_TYPE_GETNEXT ||
|
|
rtype ==
|
|
AGENTX_REQUEST_TYPE_GETNEXTINCLUSIVE))
|
|
break;
|
|
}
|
|
if (j == nvir) {
|
|
agentx_varbind_notfound(vminfo[i]);
|
|
continue;
|
|
}
|
|
mvir = &(vir[j]);
|
|
agentx_varbind_set_index_integer(vminfo[i], vmIndex,
|
|
mvir->vir_info.vir_id);
|
|
if (reqObject == vmName)
|
|
agentx_varbind_string(vminfo[i],
|
|
mvir->vir_info.vir_name);
|
|
else if (reqObject == vmUUID)
|
|
agentx_varbind_string(vminfo[i], "");
|
|
else if (reqObject == vmOSType)
|
|
agentx_varbind_string(vminfo[i], "");
|
|
else if (reqObject == vmAdminState)
|
|
agentx_varbind_integer(vminfo[i],
|
|
vm_agentx_adminstate(mvir->vir_state));
|
|
else if (reqObject == vmOperState)
|
|
agentx_varbind_integer(vminfo[i],
|
|
vm_agentx_operstate(mvir->vir_state));
|
|
else if (reqObject == vmAutoStart)
|
|
agentx_varbind_integer(vminfo[i],
|
|
mvir->vir_state & VM_STATE_DISABLED ?
|
|
3 : 2);
|
|
/* XXX We can dynamically create vm's but I don't know how to differentiate */
|
|
else if (reqObject == vmPersistent)
|
|
agentx_varbind_integer(vminfo[i], 1);
|
|
/* We currently only support a single CPU */
|
|
else if (reqObject == vmCurCpuNumber ||
|
|
reqObject == vmMinCpuNumber ||
|
|
reqObject == vmMaxCpuNumber)
|
|
agentx_varbind_integer(vminfo[i],
|
|
mvir->vir_info.vir_ncpus);
|
|
else if (reqObject == vmMemUnit)
|
|
agentx_varbind_integer(vminfo[i], MEM_SCALE);
|
|
else if (reqObject == vmCurMem)
|
|
agentx_varbind_integer(vminfo[i],
|
|
mvir->vir_info.vir_used_size / MEM_SCALE);
|
|
else if (reqObject == vmMinMem)
|
|
agentx_varbind_integer(vminfo[i], -1);
|
|
else if (reqObject == vmMaxMem)
|
|
agentx_varbind_integer(vminfo[i],
|
|
mvir->vir_info.vir_memory_size / MEM_SCALE);
|
|
/* We probably had a reload */
|
|
else
|
|
agentx_varbind_notfound(vminfo[i]);
|
|
}
|
|
vminfolen = 0;
|
|
nvir = 0;
|
|
break;
|
|
case IMSG_VMDOP_CONFIG:
|
|
config_getconfig(env, imsg);
|
|
vm_agentx_configure(&env->vmd_cfg.cfg_agentx);
|
|
break;
|
|
default:
|
|
return (-1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
void
|
|
vm_agentx_configure(struct vmd_agentx *env)
|
|
{
|
|
static char curpath[sizeof(env->ax_path)];
|
|
static char curcontext[sizeof(env->ax_context)];
|
|
static struct conn *conn;
|
|
static struct agentx_session *sess;
|
|
static struct agentx_context *ctx;
|
|
struct agentx_region *vmMIB;
|
|
char *context = env->ax_context[0] == '\0' ? NULL : env->ax_context;
|
|
int changed = 0;
|
|
|
|
vmd_agentx = env;
|
|
|
|
agentx_log_fatal = fatalx;
|
|
agentx_log_warn = log_warnx;
|
|
agentx_log_info = log_info;
|
|
agentx_log_debug = log_debug;
|
|
|
|
if (!vmd_agentx->ax_enabled) {
|
|
if (conn != NULL) {
|
|
agentx_free(conn->agentx);
|
|
conn = NULL;
|
|
sess = NULL;
|
|
ctx = NULL;
|
|
vm_agentx_flush_pending();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (strcmp(curpath, vmd_agentx->ax_path) != 0 || conn == NULL) {
|
|
if (conn != NULL) {
|
|
agentx_free(conn->agentx);
|
|
conn = NULL;
|
|
sess = NULL;
|
|
ctx = NULL;
|
|
vm_agentx_flush_pending();
|
|
}
|
|
|
|
if ((conn = malloc(sizeof(*conn))) == NULL)
|
|
fatal(NULL);
|
|
/* Set to something so we can safely event_del */
|
|
evtimer_set(&conn->ev, vm_agentx_tryconnect, conn);
|
|
/* result assigned in vm_agentx_nofd */
|
|
if (agentx(vm_agentx_nofd, conn) == NULL)
|
|
fatal("Can't setup agentx");
|
|
sess = agentx_session(conn->agentx, NULL, 0, "vmd", 0);
|
|
if (sess == NULL)
|
|
fatal("Can't setup agentx session");
|
|
(void) strlcpy(curpath, vmd_agentx->ax_path, sizeof(curpath));
|
|
changed = 1;
|
|
}
|
|
|
|
if (strcmp(curcontext, vmd_agentx->ax_context) != 0 || changed) {
|
|
if (!changed) {
|
|
agentx_context_free(ctx);
|
|
vm_agentx_flush_pending();
|
|
}
|
|
if ((ctx = agentx_context(sess, context)) == NULL)
|
|
fatal("Can't setup agentx context");
|
|
strlcpy(curcontext, vmd_agentx->ax_context, sizeof(curcontext));
|
|
changed = 1;
|
|
}
|
|
|
|
if (!changed)
|
|
return;
|
|
|
|
if ((vmMIB = agentx_region(ctx, AGENTX_OID(VMMIB), 1)) == NULL)
|
|
fatal("agentx_region vmMIB");
|
|
|
|
if ((vmIndex = agentx_index_integer_dynamic(vmMIB,
|
|
AGENTX_OID(VMINDEX))) == NULL)
|
|
fatal("agentx_index_integer_dynamic");
|
|
if ((agentx_object(vmMIB, AGENTX_OID(VMHVSOFTWARE), NULL, 0, 0,
|
|
vm_agentx_vmHvSoftware)) == NULL ||
|
|
(agentx_object(vmMIB, AGENTX_OID(VMHVVERSION), NULL, 0, 0,
|
|
vm_agentx_vmHvVersion)) == NULL ||
|
|
(agentx_object(vmMIB, AGENTX_OID(VMHVOBJECTID), NULL, 0, 0,
|
|
vm_agentx_vmHvObjectID)) == NULL ||
|
|
(vmNumber = agentx_object(vmMIB, AGENTX_OID(VMNUMBER), NULL, 0, 0,
|
|
vm_agentx_vminfo)) == NULL ||
|
|
(vmName = agentx_object(vmMIB, AGENTX_OID(VMNAME), &vmIndex, 1, 0,
|
|
vm_agentx_vminfo)) == NULL ||
|
|
(vmUUID = agentx_object(vmMIB, AGENTX_OID(VMUUID), &vmIndex, 1, 0,
|
|
vm_agentx_vminfo)) == NULL ||
|
|
(vmOSType = agentx_object(vmMIB, AGENTX_OID(VMOSTYPE), &vmIndex, 1,
|
|
0, vm_agentx_vminfo)) == NULL ||
|
|
(vmAdminState = agentx_object(vmMIB, AGENTX_OID(VMADMINSTATE),
|
|
&vmIndex, 1, 0, vm_agentx_vminfo)) == NULL ||
|
|
(vmOperState = agentx_object(vmMIB, AGENTX_OID(VMOPERSTATE),
|
|
&vmIndex, 1, 0, vm_agentx_vminfo)) == NULL ||
|
|
(vmAutoStart = agentx_object(vmMIB, AGENTX_OID(VMAUTOSTART),
|
|
&vmIndex, 1, 0, vm_agentx_vminfo)) == NULL ||
|
|
(vmPersistent = agentx_object(vmMIB, AGENTX_OID(VMPERSISTENT),
|
|
&vmIndex, 1, 0, vm_agentx_vminfo)) == NULL ||
|
|
(vmCurCpuNumber = agentx_object(vmMIB, AGENTX_OID(VMCURCPUNUMBER),
|
|
&vmIndex, 1, 0, vm_agentx_vminfo)) == NULL ||
|
|
(vmMinCpuNumber = agentx_object(vmMIB, AGENTX_OID(VMMINCPUNUMBER),
|
|
&vmIndex, 1, 0, vm_agentx_vminfo)) == NULL ||
|
|
(vmMaxCpuNumber = agentx_object(vmMIB, AGENTX_OID(VMMAXCPUNUMBER),
|
|
&vmIndex, 1, 0, vm_agentx_vminfo)) == NULL ||
|
|
(vmMemUnit = agentx_object(vmMIB, AGENTX_OID(VMMEMUNIT),
|
|
&vmIndex, 1, 0, vm_agentx_vminfo)) == NULL ||
|
|
(vmCurMem = agentx_object(vmMIB, AGENTX_OID(VMCURMEM),
|
|
&vmIndex, 1, 0, vm_agentx_vminfo)) == NULL ||
|
|
(vmMinMem = agentx_object(vmMIB, AGENTX_OID(VMMINMEM),
|
|
&vmIndex, 1, 0, vm_agentx_vminfo)) == NULL ||
|
|
(vmMaxMem = agentx_object(vmMIB, AGENTX_OID(VMMAXMEM),
|
|
&vmIndex, 1, 0, vm_agentx_vminfo)) == NULL)
|
|
fatal("agentx_object_ro");
|
|
}
|
|
|
|
static void
|
|
vm_agentx_nofd(struct agentx *agentx, void *cookie, int close)
|
|
{
|
|
struct conn *conn = cookie;
|
|
|
|
conn->agentx = agentx;
|
|
event_del(&conn->ev);
|
|
if (close)
|
|
free(conn);
|
|
else
|
|
vm_agentx_tryconnect(-1, 0, conn);
|
|
}
|
|
|
|
static void
|
|
vm_agentx_tryconnect(int fd, short event, void *cookie)
|
|
{
|
|
struct sockaddr_un sun;
|
|
struct timeval timeout = {3, 0};
|
|
struct conn *conn = cookie;
|
|
|
|
sun.sun_len = sizeof(sun);
|
|
sun.sun_family = AF_UNIX;
|
|
strlcpy(sun.sun_path, vmd_agentx->ax_path, sizeof(sun.sun_path));
|
|
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
|
|
log_warn("socket");
|
|
goto fail;
|
|
} else if (connect(fd, (struct sockaddr *)&sun, sun.sun_len) == -1) {
|
|
log_warn("connect");
|
|
close(fd);
|
|
goto fail;
|
|
}
|
|
agentx_connect(conn->agentx, fd);
|
|
|
|
event_set(&conn->ev, fd, EV_READ|EV_PERSIST, vm_agentx_read, conn);
|
|
event_add(&conn->ev, NULL);
|
|
|
|
return;
|
|
fail:
|
|
evtimer_set(&conn->ev, vm_agentx_tryconnect, conn);
|
|
evtimer_add(&conn->ev, &timeout);
|
|
}
|
|
|
|
static void
|
|
vm_agentx_read(int fd, short event, void *cookie)
|
|
{
|
|
struct conn *conn = cookie;
|
|
|
|
agentx_read(conn->agentx);
|
|
}
|
|
|
|
static void
|
|
vm_agentx_flush_pending(void)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < vminfolen; i++)
|
|
agentx_varbind_notfound(vminfo[i]);
|
|
vminfolen = 0;
|
|
}
|
|
|
|
static int
|
|
vm_agentx_sortvir(const void *c1, const void *c2)
|
|
{
|
|
const struct vmop_info_result *v1 = c1, *v2 = c2;
|
|
|
|
return (v1->vir_info.vir_id < v2->vir_info.vir_id ? -1 :
|
|
v1->vir_info.vir_id > v2->vir_info.vir_id);
|
|
}
|
|
|
|
static int
|
|
vm_agentx_adminstate(int mask)
|
|
{
|
|
if (mask & VM_STATE_PAUSED)
|
|
return (3);
|
|
else if (mask & VM_STATE_RUNNING)
|
|
return (1);
|
|
else if (mask & VM_STATE_SHUTDOWN)
|
|
return (4);
|
|
/* Presence of absence of other flags */
|
|
else if (!mask || (mask & VM_STATE_DISABLED))
|
|
return (4);
|
|
|
|
return 4;
|
|
}
|
|
|
|
static int
|
|
vm_agentx_operstate(int mask)
|
|
{
|
|
if (mask & VM_STATE_PAUSED)
|
|
return (8);
|
|
else if (mask & VM_STATE_RUNNING)
|
|
return (4);
|
|
else if (mask & VM_STATE_SHUTDOWN)
|
|
return (11);
|
|
/* Presence of absence of other flags */
|
|
else if (!mask || (mask & VM_STATE_DISABLED))
|
|
return (11);
|
|
|
|
return (11);
|
|
}
|
|
|
|
static void
|
|
vm_agentx_vmHvSoftware(struct agentx_varbind *vb)
|
|
{
|
|
agentx_varbind_string(vb, "OpenBSD VMM");
|
|
}
|
|
|
|
static void
|
|
vm_agentx_vmHvVersion(struct agentx_varbind *vb)
|
|
{
|
|
int osversid[] = {CTL_KERN, KERN_OSRELEASE};
|
|
static char osvers[10] = "";
|
|
size_t osverslen;
|
|
|
|
if (osvers[0] == '\0') {
|
|
osverslen = sizeof(osvers);
|
|
if (sysctl(osversid, 2, osvers, &osverslen, NULL,
|
|
0) == -1) {
|
|
log_warn("Failed vmHvVersion sysctl");
|
|
agentx_varbind_string(vb, "unknown");
|
|
return;
|
|
}
|
|
if (osverslen >= sizeof(osvers))
|
|
osverslen = sizeof(osvers) - 1;
|
|
osvers[osverslen] = '\0';
|
|
}
|
|
agentx_varbind_string(vb, osvers);
|
|
}
|
|
|
|
static void
|
|
vm_agentx_vmHvObjectID(struct agentx_varbind *vb)
|
|
{
|
|
agentx_varbind_oid(vb, AGENTX_OID(0, 0));
|
|
}
|
|
|
|
static void
|
|
vm_agentx_vminfo(struct agentx_varbind *vb)
|
|
{
|
|
extern struct vmd *env;
|
|
struct agentx_varbind **tvminfo;
|
|
|
|
if (vminfolen >= vminfosize) {
|
|
if ((tvminfo = reallocarray(vminfo, vminfosize + 10,
|
|
sizeof(*vminfo))) == NULL) {
|
|
log_warn("%s: Couldn't retrieve vm information",
|
|
__func__);
|
|
agentx_varbind_error(vb);
|
|
return;
|
|
}
|
|
vminfo = tvminfo;
|
|
vminfosize += 10;
|
|
}
|
|
|
|
if (!vmcollecting) {
|
|
if (proc_compose_imsg(&(env->vmd_ps), PROC_PARENT, -1,
|
|
IMSG_VMDOP_GET_INFO_VM_REQUEST, IMSG_AGENTX_PEERID,
|
|
-1, NULL, 0) == -1) {
|
|
log_warn("%s: Couldn't retrieve vm information",
|
|
__func__);
|
|
agentx_varbind_error(vb);
|
|
return;
|
|
}
|
|
vmcollecting = 1;
|
|
}
|
|
|
|
vminfo[vminfolen++] = vb;
|
|
}
|