Skip to content

Commit 82d9bea

Browse files
committed
feat(server): add a --replace option
Closes #529 which seemed to be a common complaint among teachers. * `learn-ocaml serve --replace` will kill an existing server (running on the same port) just before starting * `learn-ocaml build serve` with an existing server on the same port will fail fast (before actually doing the build) * `learn-ocaml build serve --replace` is more clever: - it will do the build *in a temporary directory* - then, only if everything is ok, kill the older server - swap the files and start the new server This is all done in order to minimise downtime and be convenient for server updates. Note that this PR sits on top of #481 and should be rebased once it's merged.
1 parent 6583af4 commit 82d9bea

7 files changed

+155
-21
lines changed

src/main/learnocaml_main.ml

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,15 @@ let process_html_file orig_file dest_file base_url no_secret =
292292
Lwt_io.close ofile >>= fun () ->
293293
Lwt_io.close wfile
294294

295+
let temp_app_dir o =
296+
let open Filename in
297+
concat
298+
(dirname o.app_dir)
299+
((basename o.app_dir) ^ ".temp")
300+
295301
let main o =
296-
Printf.printf "Learnocaml v.%s running.\n" Learnocaml_api.version;
297-
let grade () =
302+
Printf.printf "Learnocaml v.%s running.\n%!" Learnocaml_api.version;
303+
let grade o =
298304
if List.mem Grade o.commands then
299305
(if List.mem Build o.commands || List.mem Serve o.commands then
300306
failwith "The 'grade' command is incompatible with 'build' and \
@@ -322,9 +328,34 @@ let main o =
322328
>|= fun i -> Some i)
323329
else Lwt.return_none
324330
in
325-
let generate () =
331+
let generate o =
326332
if List.mem Build o.commands then
327-
(Printf.printf "Updating app at %s\n%!" o.app_dir;
333+
(let get_app_dir o =
334+
if not (List.mem Serve o.commands) then
335+
Lwt.return o.app_dir
336+
else if o.server.Server.replace then
337+
let app_dir = temp_app_dir o in
338+
(if Sys.file_exists app_dir then
339+
(Printf.eprintf "Warning: temporary directory %s already exists\n%!"
340+
app_dir;
341+
Lwt.return_unit)
342+
else if Sys.file_exists o.app_dir then
343+
Lwt_utils.copy_tree o.app_dir app_dir
344+
else
345+
Lwt.return_unit)
346+
>>= fun () -> Lwt.return app_dir
347+
else if Learnocaml_server.check_running () <> None then
348+
(Printf.eprintf
349+
"Error: another server is already running on port %d \
350+
(consider using option `--replace`)\n%!"
351+
!Learnocaml_server.port;
352+
exit 10)
353+
else Lwt.return o.app_dir
354+
in
355+
get_app_dir o >>= fun app_dir ->
356+
let o = { o with app_dir } in
357+
Learnocaml_store.static_dir := app_dir;
358+
Printf.printf "Updating app at %s\n%!" o.app_dir;
328359
Lwt.catch
329360
(fun () -> Lwt_utils.copy_tree o.builder.Builder.contents_dir o.app_dir)
330361
(function
@@ -404,8 +435,44 @@ let main o =
404435
else
405436
Lwt.return true
406437
in
407-
let run_server () =
438+
let run_server o =
408439
if List.mem Serve o.commands then
440+
let () =
441+
if o.server.Server.replace then
442+
let running = Learnocaml_server.check_running () in
443+
Option.iter Learnocaml_server.kill_running running;
444+
let temp = temp_app_dir o in
445+
let app_dir =
446+
if Filename.is_relative o.app_dir
447+
then Filename.concat (Sys.getcwd ()) o.app_dir
448+
else o.app_dir
449+
in
450+
let bak =
451+
let f =
452+
Filename.temp_file
453+
~temp_dir:(Filename.dirname app_dir)
454+
(Filename.basename app_dir ^ ".bak.")
455+
""
456+
in
457+
Unix.unlink f; f
458+
in
459+
if Sys.file_exists app_dir then Sys.rename app_dir bak;
460+
Sys.rename temp o.app_dir;
461+
Learnocaml_store.static_dir := app_dir;
462+
if Sys.file_exists bak then
463+
Lwt.dont_wait (fun () ->
464+
Lwt.pause () >>= fun () ->
465+
Lwt_process.exec ("rm",[|"rm";"-rf";bak|]) >>= fun r ->
466+
if r <> Unix.WEXITED 0 then
467+
Lwt.fail_with "Remove command failed"
468+
else Lwt.return_unit
469+
)
470+
(fun ex ->
471+
Printf.eprintf
472+
"Warning: while cleaning up older application \
473+
directory %s:\n %s\n%!"
474+
bak (Printexc.to_string ex))
475+
in
409476
let native_server = Sys.executable_name ^ "-server" in
410477
if Sys.file_exists native_server then
411478
let server_args =
@@ -416,30 +483,39 @@ let main o =
416483
("--port="^string_of_int o.server.port) ::
417484
(match o.server.cert with None -> [] | Some c -> ["--cert="^c])
418485
in
419-
Unix.execv native_server (Array.of_list (native_server::server_args))
486+
Lwt.return
487+
(`Continuation
488+
(fun () ->
489+
Unix.execv native_server
490+
(Array.of_list (native_server::server_args))))
420491
else begin
421492
Printf.printf "Starting server on port %d\n%!"
422493
!Learnocaml_server.port;
423494
if o.builder.Builder.base_url <> "" then
424495
Printf.printf "Base URL: %s\n%!" o.builder.Builder.base_url;
425-
Learnocaml_server.launch ()
496+
Learnocaml_server.launch () >>= fun ret ->
497+
Lwt.return (`Success ret)
426498
end
427499
else
428-
Lwt.return true
500+
Lwt.return (`Success true)
429501
in
430502
let ret =
431503
Lwt_main.run
432-
(grade () >>= function
433-
| Some i -> Lwt.return i
504+
(grade o >>= function
505+
| Some i -> Lwt.return (`Code i)
434506
| None ->
435-
generate () >>= fun success ->
507+
generate o >>= fun success ->
436508
if success then
437-
run_server () >>= fun r ->
438-
if r then Lwt.return 0 else Lwt.return 10
509+
run_server o >>= function
510+
| `Success true -> Lwt.return (`Code 0)
511+
| `Success false -> Lwt.return (`Code 10)
512+
| `Continuation f -> Lwt.return (`Continuation f)
439513
else
440-
Lwt.return 1)
514+
Lwt.return (`Code 1))
441515
in
442-
exit ret
516+
match ret with
517+
| `Code n -> exit n
518+
| `Continuation f -> f ()
443519

444520
let man =
445521
let open Manpage in

src/main/learnocaml_server_args.ml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module type S = sig
1919
base_url: string;
2020
port: int;
2121
cert: string option;
22+
replace: bool;
2223
}
2324

2425
val term: string Cmdliner.Term.t -> string Cmdliner.Term.t -> t Cmdliner.Term.t
@@ -51,15 +52,21 @@ module Args (SN : Section_name) = struct
5152
HTTPS is enabled."
5253
default_http_port default_https_port)
5354

55+
let replace =
56+
value & flag &
57+
info ["replace"] ~doc:
58+
"Replace a previously running instance of the server on the same port."
59+
5460
type t = {
5561
sync_dir: string;
5662
base_url: string;
5763
port: int;
5864
cert: string option;
65+
replace: bool;
5966
}
6067

6168
let term app_dir base_url =
62-
let apply app_dir sync_dir base_url port cert =
69+
let apply app_dir sync_dir base_url port cert replace =
6370
Learnocaml_store.static_dir := app_dir;
6471
Learnocaml_store.sync_dir := sync_dir;
6572
let port = match port, cert with
@@ -73,10 +80,10 @@ module Args (SN : Section_name) = struct
7380
| None -> None);
7481
Learnocaml_server.port := port;
7582
Learnocaml_server.base_url := base_url;
76-
{ sync_dir; base_url; port; cert }
83+
{ sync_dir; base_url; port; cert; replace }
7784
in
7885
(* warning: if you add any options here, remember to pass them through when
7986
calling the native server from learn-ocaml main *)
80-
Term.(const apply $ app_dir $ sync_dir $ base_url $ port $ cert)
87+
Term.(const apply $ app_dir $ sync_dir $ base_url $ port $ cert $ replace)
8188

8289
end

src/main/learnocaml_server_args.mli

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ module type S = sig
1616
base_url: string;
1717
port: int;
1818
cert: string option;
19+
replace: bool;
1920
}
2021

2122
val term: string Cmdliner.Term.t -> string Cmdliner.Term.t -> t Cmdliner.Term.t
2223
end
2324

24-
module Args : functor (_ : Section_name) -> S
25+
module Args : functor (_ : Section_name) -> S

src/main/learnocaml_server_main.ml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ let main o =
3131
Learnocaml_api.version o.port;
3232
if o.base_url <> "" then
3333
Printf.printf "Base URL: %s\n%!" o.base_url;
34+
let () =
35+
match Learnocaml_server.check_running (), o.replace with
36+
| None, _ -> ()
37+
| Some _, false ->
38+
Printf.eprintf "Error: another server is already running on port %d \
39+
(consider using option `--replace`)\n%!"
40+
!Learnocaml_server.port;
41+
exit 10
42+
| Some pid, true ->
43+
Learnocaml_server.kill_running pid
44+
in
3445
let rec run () =
3546
let minimum_duration = 15. in
3647
let t0 = Unix.time () in

src/repo/learnocaml_process_exercise_repository.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ let spawn_grader
6262
?print_result ?dirname meta ex_dir output_json =
6363
let rec sleep () =
6464
if !n_processes <= 0 then
65-
Lwt_main.yield () >>= sleep
65+
Lwt.pause () >>= sleep
6666
else (
6767
decr n_processes; Lwt.return_unit
6868
)

src/server/learnocaml_server.ml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,3 +715,35 @@ let launch () =
715715
| e ->
716716
Printf.eprintf "Server error: %s\n%!" (Printexc.to_string e);
717717
Lwt.return false
718+
719+
let check_running () =
720+
try
721+
let ic = Printf.ksprintf Unix.open_process_in "lsof -Qti tcp:%d -s tcp:LISTEN" !port in
722+
let pid = match input_line ic with
723+
| "" -> None
724+
| s -> int_of_string_opt s
725+
| exception End_of_file -> None
726+
in
727+
close_in ic;
728+
pid
729+
with Unix.Unix_error _ ->
730+
Printf.eprintf "Warning: could not check for previously running instance";
731+
None
732+
733+
let kill_running pid =
734+
let timeout = 15 in
735+
Unix.kill pid Sys.sigint;
736+
Printf.eprintf "Waiting for process %d to terminate... %2d%!" pid timeout;
737+
let rec aux tout =
738+
Printf.eprintf "\027[2D%2d" tout;
739+
if Printf.ksprintf Sys.command "lsof -ti tcp:%d -p %d >/dev/null" !port pid
740+
= 0
741+
then
742+
if tout <= 0 then
743+
(prerr_endline "Error: process didn't terminate in time"; exit 10)
744+
else
745+
(Unix.sleep 1;
746+
aux (tout - 1))
747+
in
748+
aux timeout;
749+
prerr_endline "\027[2Dok"

src/server/learnocaml_server.mli

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,12 @@ val args: (Arg.key * Arg.spec * Arg.doc) list
1616

1717
(** Main *)
1818

19-
(* Returns [false] if interrupted prematurely due to an error *)
19+
val check_running: unit -> int option
20+
(** Returns the pid or an existing process listening on the tcp port *)
21+
22+
val kill_running: int -> unit
23+
(** Kills the given process and waits for termination (fails upon
24+
reaching a timeout) *)
25+
2026
val launch: unit -> bool Lwt.t
27+
(** Returns [false] if interrupted prematurely due to an error *)

0 commit comments

Comments
 (0)