Skip to content

Commit 6cee13c

Browse files
authored
Merge pull request #567 from AltGr/server-replace
feat(server): add a `--replace` option
2 parents 6356328 + 995a79d commit 6cee13c

File tree

7 files changed

+188
-30
lines changed

7 files changed

+188
-30
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: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ let dump_dot exs =
4747

4848
let n_processes = ref 1
4949

50+
let grading_status, grading_status_add, grading_status_remove =
51+
let in_progress = ref [] in
52+
let tty = Unix.isatty Unix.stderr in
53+
let show () =
54+
match !in_progress with
55+
| [] -> flush stderr
56+
| prog ->
57+
Printf.eprintf "Grading in progress: %s" (String.concat " " prog);
58+
if tty then (flush stderr; prerr_string "\r\027[K") else prerr_newline ()
59+
in
60+
show,
61+
(fun id -> in_progress := !in_progress @ [id]; show ()),
62+
(fun id ->
63+
in_progress := List.filter (fun x -> not (String.equal x id)) !in_progress;
64+
show ())
65+
5066
let print_grader_error exercise = function
5167
| Ok () -> ()
5268
| Error (-1) -> ()
@@ -59,25 +75,29 @@ let print_grader_error exercise = function
5975

6076
let spawn_grader
6177
dump_outputs dump_reports
62-
?print_result ?dirname meta exercise output_json =
78+
?print_result ?dirname id meta ex_dir output_json =
6379
let rec sleep () =
6480
if !n_processes <= 0 then
65-
Lwt_main.yield () >>= sleep
81+
Lwt.pause () >>= sleep
6682
else (
6783
decr n_processes; Lwt.return_unit
6884
)
6985
in
7086
sleep () >>= fun () ->
7187
Lwt.catch (fun () ->
88+
read_exercise ex_dir >>= fun exercise ->
89+
grading_status_add id;
7290
Grader_cli.grade
7391
~dump_outputs ~dump_reports ~display_callback:false
7492
?print_result ?dirname meta exercise output_json
7593
>|= fun r ->
94+
grading_status_remove id;
7695
print_grader_error exercise r;
7796
incr n_processes;
7897
r)
7998
(fun e ->
8099
incr n_processes;
100+
grading_status_remove id;
81101
Printf.eprintf "Grader error: %s\n%!" (Printexc.to_string e);
82102
Lwt.return (Error 0))
83103

@@ -213,7 +233,8 @@ let main dest_dir =
213233
if !n_processes = 1 then
214234
Lwt_list.map_s,
215235
fun dump_outputs dump_reports ?print_result ?dirname
216-
meta exercise json_path ->
236+
_id meta ex_dir json_path ->
237+
read_exercise ex_dir >>= fun exercise ->
217238
Grader_cli.grade
218239
~dump_outputs ~dump_reports ~display_callback:true
219240
?print_result ?dirname
@@ -234,28 +255,31 @@ let main dest_dir =
234255
else Lwt.return_unit)
235256
(Lwt_unix.files_of_directory ex_dir) >>= fun () ->
236257
if not changed then begin
237-
Format.printf "%-24s (no changes)@." id ;
258+
Format.eprintf "%-24s (no changes)@." id ;
238259
Lwt.return_true
239260
end else begin
240261
Learnocaml_precompile_exercise.precompile ~exercise_dir:ex_dir
241262
>>= fun () ->
242-
read_exercise ex_dir
243-
>>= fun exercise ->
244263
grade dump_outputs dump_reports
245-
~dirname:ex_dir (Index.find index id) exercise (Some json_path)
264+
~dirname:ex_dir id (Index.find index id) ex_dir (Some json_path)
246265
>>= function
247266
| Ok () ->
248-
Format.printf "%-24s [OK]@." id ;
267+
Format.eprintf "%-24s [OK]@." id ;
249268
Lwt.return true
250269
| Error _ ->
251-
Format.printf "%-24s [FAILED]@." id ;
270+
Format.eprintf "%-24s [FAILED]@." id ;
252271
Lwt.return false
253-
end)
272+
end
273+
>|= fun r -> grading_status (); r)
254274
processes_arguments
255275
end >>= fun results ->
256276
Lwt.return (List.for_all ((=) true) results))
257277
(fun exn ->
258278
let print_unknown ppf = function
279+
| Unix.Unix_error (Unix.EMFILE, _, _) ->
280+
Format.fprintf ppf
281+
"Too many open files. Try reducing the number of concurrent jobs \
282+
(with the `-j` flag) or use `ulimit -n` with a higher value"
259283
| Failure msg -> Format.fprintf ppf "Cannot process exercises: %s" msg
260284
| exn -> Format.fprintf ppf "Cannot process exercises: %s" (Printexc.to_string exn) in
261285
Json_encoding.print_error ~print_unknown Format.err_formatter exn ;

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)