Skip to content

Commit c730416

Browse files
stephenchengCloudStephen Cheng
authored andcommitted
CP-54393: VNC console idle timeout
This commit adds idle timeout feature for vnc console connections. Key changes: - Add idle timeout detection by monitoring RFB keyEvent and pointerEvent. - Add callback function to `proxy` to parse the RFB messages and determine if the connection is idle or not. Signed-off-by: Stephen Cheng <[email protected]>
1 parent 2f461c7 commit c730416

File tree

4 files changed

+132
-9
lines changed

4 files changed

+132
-9
lines changed

ocaml/libs/xapi-stdext/lib/xapi-stdext-unix/unixext.ml

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,8 @@ module CBuf = struct
338338
in
339339
let read = Unix.read fd x.buffer next len in
340340
if read = 0 then x.r_closed <- true ;
341-
x.len <- x.len + read
341+
x.len <- x.len + read ;
342+
(x.buffer, read, next)
342343
end
343344

344345
exception Process_still_alive
@@ -381,11 +382,21 @@ let with_polly f =
381382
let finally () = Polly.close polly in
382383
Xapi_stdext_pervasives.Pervasiveext.finally (fun () -> f polly) finally
383384

384-
let proxy (a : Unix.file_descr) (b : Unix.file_descr) =
385+
exception Close_proxy
386+
387+
let proxy ?should_close ?(poll_timeout = -1) (a : Unix.file_descr)
388+
(b : Unix.file_descr) =
385389
let size = 64 * 1024 in
386390
(* [a'] is read from [a] and will be written to [b] *)
387391
(* [b'] is read from [b] and will be written to [a] *)
388392
let a' = CBuf.empty size and b' = CBuf.empty size in
393+
394+
let close_proxy () =
395+
Unix.shutdown a Unix.SHUTDOWN_ALL ;
396+
Unix.shutdown b Unix.SHUTDOWN_ALL ;
397+
raise Close_proxy
398+
in
399+
389400
Unix.set_nonblock a ;
390401
Unix.set_nonblock b ;
391402
with_polly @@ fun polly ->
@@ -413,13 +424,22 @@ let proxy (a : Unix.file_descr) (b : Unix.file_descr) =
413424
Polly.upd polly a a_events ;
414425
if Polly.Events.(b_events <> empty) then
415426
Polly.upd polly b b_events ;
416-
Polly.wait_fold polly 4 (-1) () (fun _polly fd events () ->
427+
Polly.wait_fold polly 4 poll_timeout (Bytes.empty, 0, 0)
428+
(fun _polly fd events acc ->
417429
(* Do the writing before the reading *)
418430
if Polly.Events.(test out events) then
419431
if a = fd then CBuf.write b' a else CBuf.write a' b ;
420432
if Polly.Events.(test inp events) then
421-
if a = fd then CBuf.read a' a else CBuf.read b' b
422-
) ;
433+
if a = fd then (
434+
ignore (CBuf.read a' a) ;
435+
acc
436+
) else
437+
CBuf.read b' b
438+
else
439+
acc
440+
)
441+
|> fun data ->
442+
Option.iter (fun cb -> if cb data then close_proxy ()) should_close ;
423443
(* If there's nothing else to read or write then signal the other end *)
424444
List.iter
425445
(fun (buf, fd) ->

ocaml/libs/xapi-stdext/lib/xapi-stdext-unix/unixext.mli

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,12 @@ exception Process_still_alive
126126

127127
val kill_and_wait : ?signal:int -> ?timeout:float -> int -> unit
128128

129-
val proxy : Unix.file_descr -> Unix.file_descr -> unit
129+
val proxy :
130+
?should_close:(bytes * int * int -> bool)
131+
-> ?poll_timeout:int
132+
-> Unix.file_descr
133+
-> Unix.file_descr
134+
-> unit
130135

131136
val really_read : Unix.file_descr -> bytes -> int -> int -> unit
132137

ocaml/xapi/console.ml

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,87 @@ let address_of_console __context console : address option =
126126
) ;
127127
address_option
128128

129-
let real_proxy' vnc_port s =
129+
module Console_idle_monitor = struct
130+
let get_idle_timeout_config ~__context ~vm =
131+
try
132+
let idle_timeout =
133+
if Db.VM.get_is_control_domain ~__context ~self:vm then
134+
let host = Helpers.get_localhost ~__context in
135+
Db.Host.get_console_idle_timeout ~__context ~self:host
136+
else
137+
let pool = Helpers.get_pool ~__context in
138+
Db.Pool.get_vm_console_idle_timeout ~__context ~self:pool
139+
in
140+
if idle_timeout > 0L then Some (Int64.to_float idle_timeout) else None
141+
with _ -> None
142+
143+
let is_active messages =
144+
List.exists
145+
(function
146+
| Rfb_client_msgtype_parser.KeyEvent
147+
| Rfb_client_msgtype_parser.PointerEvent
148+
| Rfb_client_msgtype_parser.QEMUClientMessage ->
149+
true
150+
| _ ->
151+
false
152+
)
153+
messages
154+
155+
let timed_out ~idle_timeout_seconds ~last_activity =
156+
let elapsed = Mtime_clock.count last_activity in
157+
Mtime.Span.to_float_ns elapsed /. 1e9 > idle_timeout_seconds
158+
159+
(* Create an idle timeout callback for the console,
160+
if idle timeout, then close the proxy *)
161+
let create_idle_timeout_callback ~__context ~vm =
162+
match get_idle_timeout_config ~__context ~vm with
163+
| Some idle_timeout_seconds -> (
164+
let module P = Rfb_client_msgtype_parser in
165+
let state = ref (Some (P.create (), Mtime_clock.counter ())) in
166+
(* Return true for idle timeout to close the proxy,
167+
otherwise return false to keep the proxy open *)
168+
fun (buf, read_len, offset) ->
169+
match !state with
170+
| None ->
171+
false
172+
| Some (rfb_parser, last_activity) ->
173+
let ok msgs =
174+
if is_active msgs then (
175+
state := Some (rfb_parser, Mtime_clock.counter ()) ;
176+
false
177+
) else
178+
let timeout_result =
179+
timed_out ~idle_timeout_seconds ~last_activity
180+
in
181+
if timeout_result then
182+
debug
183+
"Console connection idle timeout exceeded for VM %s \
184+
(timeout: %.1fs)"
185+
(Ref.string_of vm) idle_timeout_seconds ;
186+
timeout_result
187+
in
188+
let error msg =
189+
debug "RFB parse error: %s" msg ;
190+
state := None ;
191+
false
192+
in
193+
Bytes.sub_string buf offset read_len
194+
|> rfb_parser
195+
|> Result.fold ~ok ~error
196+
)
197+
| None ->
198+
Fun.const false
199+
end
200+
201+
let get_poll_timeout =
202+
let poll_period_timeout = !Xapi_globs.proxy_poll_period_timeout in
203+
if poll_period_timeout < 0. then
204+
-1
205+
else
206+
Float.to_int (poll_period_timeout *. 1000.)
207+
(* convert to milliseconds *)
208+
209+
let real_proxy' ~__context ~vm vnc_port s =
130210
try
131211
Http_svr.headers s (Http.http_200_ok ()) ;
132212
let vnc_sock =
@@ -141,7 +221,12 @@ let real_proxy' vnc_port s =
141221
debug "Connected; running proxy (between fds: %d and %d)"
142222
(Unixext.int_of_file_descr vnc_sock)
143223
(Unixext.int_of_file_descr s') ;
144-
Unixext.proxy vnc_sock s' ;
224+
225+
let poll_timeout = get_poll_timeout in
226+
let should_close =
227+
Console_idle_monitor.create_idle_timeout_callback ~__context ~vm
228+
in
229+
Unixext.proxy ~should_close ~poll_timeout vnc_sock s' ;
145230
debug "Proxy exited"
146231
with exn -> debug "error: %s" (ExnHelper.string_of_exn exn)
147232

@@ -153,7 +238,7 @@ let real_proxy __context vm _ _ vnc_port s =
153238
in
154239
if Connection_limit.try_add vm_id is_limit_enabled then
155240
finally (* Ensure we drop the vm connection count if exceptions occur *)
156-
(fun () -> real_proxy' vnc_port s)
241+
(fun () -> real_proxy' ~__context ~vm vnc_port s)
157242
(fun () -> Connection_limit.drop vm_id)
158243
else
159244
Http_svr.headers s (Http.http_503_service_unavailable ())

ocaml/xapi/xapi_globs.ml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,11 @@ let xha_timeout = "timeout"
450450

451451
let message_limit = ref 10000
452452

453+
(* The timeout (in seconds) for event polling in the proxy loop.
454+
If set to a positive value, the poll will wake up periodically,
455+
which is useful for implementing features like idle timeout or periodic inspection of proxy buffers. *)
456+
let proxy_poll_period_timeout = ref 5.0
457+
453458
let xapi_message_script = ref "mail-alarm"
454459

455460
(* Emit a warning if more than this amount of clock skew detected *)
@@ -1783,6 +1788,14 @@ let other_options =
17831788
, (fun () -> string_of_float !vm_sysprep_wait)
17841789
, "Time in seconds to wait for VM to recognise inserted CD"
17851790
)
1791+
; ( "proxy_poll_period_timeout"
1792+
, Arg.Set_float proxy_poll_period_timeout
1793+
, (fun () -> string_of_float !proxy_poll_period_timeout)
1794+
, "Timeout (in seconds) for event polling in network proxy loops. When \
1795+
positive, the proxy will wake up periodically to check tasks like vnc \
1796+
idle timeouts or perform other maintenance tasks. Set to -1 to wait \
1797+
indefinitely for network events without periodic wake-ups."
1798+
)
17861799
]
17871800

17881801
(* The options can be set with the variable xapiflags in /etc/sysconfig/xapi.

0 commit comments

Comments
 (0)