-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathvmware-control.ahk
357 lines (278 loc) · 8.83 KB
/
vmware-control.ahk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
; Note: If this file contains non-ASCII characters, you must save it in UTF8 with BOM,
; in order for the Unicode characters to be recognized by Autohotkey engine.
;
AUTOEXEC_vmware_control:
; Workaround for Autohotkey's ugly auto-exec feature. Must be first line.
/*
API:
Start_MonitorPausedVMsAndSuspendThem(delay_minutes)
Stop_MonitorPausedVMsAndSuspendThem()
The Start function starts a timer to check for idle VMware workstation VMs,
(a paused VM is a typical idle one), and, if a VM is idle for more than 1 hour,
I will run "vmrun.exe suspend xxx.vmx" to suspend it.
VMware does NOT provide out-of-box command to query whether a VM is paused,
so I have to check for vmx-folder all files's modification time to deduce.
Config:
In custom_env.ahk, user can define `class VmctlCfg{ static ... }` to customize
this module's behaviors.
class VmctlCfg
{
static vmwks_exedir := "D:\Program Files (x86)\VMware\VMware Workstation"
static idle_seconds := 3600*2
static exclude_dirs := [ "L:\tempVM\Win10WTG-uefi", "L:\tempVM\Win10-WTG" ]
}
*/
vmctl_InitEnv()
return ; The first return in this ahk. It marks the End of auto-execute section.
;
; After this line, you can define hotkeys and functions,
; or #Include somebody else's AHK partial file(s).
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
class vmctl
{
static Id := "VmCtl"
static _tmp_ := AmDbg_SetDesc(vmctl.Id, "Debug messages from vmware-control.ahk")
; Cfg:
static vmwks_exedir := "C:\Program Files (x86)\VMware\VMware Workstation"
static chk_interval_seconds := 60
static delay_seconds_bfr_suspend := 3600
; -- this value is changed by Start_MonitorPausedVMsAndSuspendThem()'s argument.
static exclude_dirs := []
;
; Runtime data:
;
static dictvm := {} ; dict-key is vmxpath
static smsec_now := 0 ; self maintained seconds as time-reference
}
vmctl_InitEnv()
{
; Always start this house-keeping timer.
;
dev_StartTimerPeriodic("_vmctl_smsec_inc", 1000)
if(VmctlCfg.vmwks_exedir)
vmctl.vmwks_exedir := VmctlCfg.vmwks_exedir
if(VmctlCfg.idle_seconds)
vmctl.delay_seconds_bfr_suspend := VmctlCfg.idle_seconds
if(VmctlCfg.exclude_dirs)
vmctl.exclude_dirs := VmctlCfg.exclude_dirs
}
vmctl_dbg(msg)
{
AmDbg_Lv1(vmctl.Id, msg)
}
vmctl_dbg2(msg)
{
AmDbg_Lv2(vmctl.Id, msg)
}
_vmctl_smsec_inc()
{
; smsec: Self-maintained time-point in second.
;
; Yes, we need to measure only our program's running time elapse.
; If Windows sleeps(so all VMs are implicitly paused), we do not want to count up during the sleep.
; Timer delay accumulation error is not a matter for this scenario.
vmctl.smsec_now++
}
Start_MonitorPausedVMsAndSuspendThem(minutes:=60)
{
if(minutes==0)
minutes := 1
if(minutes>0) {
vmctl.delay_seconds_bfr_suspend := minutes * 60
}
else {
; take input 'minutes' value as seconds, a dev/debug facility
vmctl.delay_seconds_bfr_suspend := -minutes
}
; Log configurations
msg := "[Configuration]`n"
msg .= "vmwks_exedir = " vmctl.vmwks_exedir "`n"
msg .= "idle_seconds = " vmctl.delay_seconds_bfr_suspend "`n"
msg .= "exclude_dirs = `n"
for index,dir in vmctl.exclude_dirs
{
msg .= " " dir "`n"
}
vmctl_dbg(msg)
first_err := ""
if(not vmctl_CheckAndSuspendPausedVMs(first_err))
{
dev_MsgBoxError("Error occurred querying VM running state, so timer will not start.`n`n" . first_err)
return
}
dev_StartTimerPeriodic("vmctl_CheckAndSuspendPausedVMs", 1000*vmctl.chk_interval_seconds)
vmctl_dbg(Format("vmctl_CheckAndSuspendPausedVMs() timer started, check every {} seconds, delay {} seconds before suspend."
, vmctl.chk_interval_seconds, vmctl.delay_seconds_bfr_suspend))
}
Stop_MonitorPausedVMsAndSuspendThem()
{
dev_StopTimer("vmctl_CheckAndSuspendPausedVMs")
}
vmctl_is_exclude_dir(vmxpath)
{
for index,dir in vmctl.exclude_dirs
{
if(StrIsStartsWith(vmxpath, dir, true))
return true
}
return false
}
vmctl_CheckAndSuspendPausedVMs(byref errmsg:="")
{
vmxlist := vmctl_GetRunningVmxList()
if(StrIsStartsWith(vmxlist, "[ERROR]"))
{
errmsg := vmxlist
return false
}
for index,vmxpath in vmxlist
{
if(vmctl_is_exclude_dir(vmxpath))
continue
smsec_now := vmctl.smsec_now
if(not vmctl.dictvm.HasKey(vmxpath))
{
vmctl.dictvm[vmxpath] := {}
vmctl.dictvm[vmxpath].LastFiletime := "" ; will be in AHK TS14 format
vmctl.dictvm[vmxpath].smsec_idle_start := smsec_now
}
thisvm := vmctl.dictvm[vmxpath]
LastFiletime := vmctl_GetVmLastModifyTime(vmxpath, thisvm.LastFiletime)
if(not LastFiletime)
{
errmsg := Format("Error get diskfile modification time, in vmctl_GetVmLastModifyTime(""{}"")", vmxpath)
vmctl_dbg(errmsg)
dev_MsgBoxError(errmsg)
continue
}
if(LastFiletime != thisvm.LastFiletime)
{
; This means: the VM got some modification since last check.
; So, update the two time reference to now-time.
if(not thisvm.LastFiletime)
{
vmctl_dbg(Format("#{} Initial folder mtime: {}", index, dev_GetDateTimeStrCompact(".", LastFiletime)))
}
else
{
vmctl_dbg(Format("#{} Activity detected: {} -> {}", index
, dev_GetDateTimeStrCompact(".", thisvm.LastFiletime), dev_GetDateTimeStrCompact(".", LastFiletime) ))
}
thisvm.LastFiletime := LastFiletime
thisvm.smsec_idle_start := smsec_now ; consider it idle from now
}
idle_secs := smsec_now - thisvm.smsec_idle_start
remain_secs := vmctl.delay_seconds_bfr_suspend - idle_secs
if(remain_secs>0)
{
vmctl_dbg(Format("#{}: Remain {} seconds for: {}", index, remain_secs, vmxpath))
}
else
{
vmctl_dbg(Format("#{}: Expired {} seconds, now suspend: {}", index, -remain_secs, vmxpath))
msg := "Start suspending VM: " vmxpath
dev_TooltipAutoClear(msg)
vmctl_SuspendVmx(vmxpath)
msg := "Done suspending VM: " vmxpath
dev_TooltipAutoClear(msg)
vmctl_dbg(Format("#{}: VM suspend done.", index))
thisvm.smsec_idle_start := smsec_now
; -- on simulating VM Suspend, this avoids triggering frequently
}
}
return true
}
vmctl_SuspendVmx(vmxpath)
{
suspend_timeout_sec := 60
warnmsg := Format("This VM suspending has NOT finished after {} seconds, please check the VM status manually.`n`n{}"
, suspend_timeout_sec, vmxpath)
fnWarnFreeze := Func("dev_MsgBoxWarning").Bind(warnmsg)
dev_StartTimerOnce(fnWarnFreeze, suspend_timeout_sec*1000)
; For VMwks 16.2.3, we need to Unpause VM first(in case it was paused),
; to ensure VM Suspend success.
cmd := Format("""{}\vmrun.exe"" unpause ""{}""", vmctl.vmwks_exedir, vmxpath)
dev_RunWaitOne(cmd, "hide")
cmd := Format("""{}\vmrun.exe"" suspend ""{}""", vmctl.vmwks_exedir, vmxpath)
dev_RunWaitOne(cmd, "hide")
dev_StopTimer(fnWarnFreeze)
}
vmctl_GetRunningVmxList()
{
cmd := Format("""{}\vmrun.exe"" list", vmctl.vmwks_exedir)
/* Output is sth like this:
Total running VMs: 2
M:\_VMS_\pfSense1\pfSense1.vmx
N:\_vms_\Win10vwork\Win10vwork.vmx
*/
dret := dev_RunWaitOneEx(cmd, "hide")
if(dret.exitcode==0)
{
rawlines := StrSplit(dret.output, "`r`n")
vmxlist := []
for index,val in rawlines
{
if(Trim(val)=="")
continue
if(dev_IsDiskFile(val))
vmxlist.Push(val)
}
return vmxlist ; may be an empty list
}
else
{
errmsg := Format("The following shell command failed:`n`n"
. "{}`n`n"
. "Console output is:`n`n"
. "{}"
, cmd, dret.output)
return "[ERROR] " . errmsg
}
}
vmctl_GetVmLastModifyTime(vmx_filepath, ts14_old:="")
{
; vmx_filepath is a .vmx's fullpath(`vmrun.exe list` reports this)
; Check all files in the vmx's folder, the latest modification time
; of all files within, is taken as that VM's modification time.
;
; If success, return a string in YYYYMMDD...... format.
; If fail, return empty string.
if(not dev_IsDiskFile(vmx_filepath))
return ""
vmxdir := dev_SplitPath(vmx_filepath)
; tt_latest_listdir := "0"
tt_latest := "0"
latest_file := ""
s_lv2_dbg_sent := false
; Scan for all .vmdk files's modification time as clue for VM Activity.
; If a VM is paused, it vmdk files will cease being modified.
Loop, Files, % vmxdir "\*.vmdk"
{
; Note: Merely checking A_LoopFileTimeModified is not enough, whose timestamp
; may be lagged quite a bit. We need to explicitly query against the specific file.
ts14 := win32_GetFileTime(A_LoopFileFullPath)
if(ts14 > A_LoopFileTimeModified) ; Lv2 debug message for that
{
if(!s_lv2_dbg_sent)
{
vmctl_dbg2(Format("Catch: listdir filetime lagged [ fake: {} , actual: {} ] on: {}"
, ts14short(A_LoopFileTimeModified), ts14short(ts14), A_LoopFileFullPath))
s_lv2_dbg_sent := true
}
}
if(ts14 > tt_latest)
{
tt_latest := ts14
latest_file := A_LoopFileFullPath
}
if(ts14_old and ts14 > ts14_old)
{
; Enough, no need to check for more files.
tt_latest := ts14
latest_file := A_LoopFileFullPath
break
}
}
vmctl_dbg2(Format("Latest file activity: <{}> {}", ts14short(tt_latest), latest_file))
return tt_latest
}