--- /dev/null
+(*
+ * Copyright (C) 2003:
+ * Stefano Zacchiroli <zack@cs.unibo.it>
+ * for the HELM Team http://helm.cs.unibo.it/
+ *
+ * This file is part of HELM, an Hypertextual, Electronic
+ * Library of Mathematics, developed at the Computer Science
+ * Department, University of Bologna, Italy.
+ *
+ * HELM is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * HELM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with HELM; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston,
+ * MA 02111-1307, USA.
+ *
+ * For details, see the HELM World-Wide-Web page,
+ * http://helm.cs.unibo.it/
+ *)
+
+(* $Id$ *)
+
+open Hbugs_common;;
+open Hbugs_types;;
+open Printf;;
+
+exception Invalid_URL of string;;
+
+let do_nothing _ = ();;
+
+module SmartHbugs_client_gui =
+ struct
+ class ['a] oneColumnCList gtree_view ~column_type ~column_title
+ =
+ let obj =
+ ((Gobject.unsafe_cast gtree_view#as_widget) : Gtk.tree_view Gtk.obj) in
+ let columns = new GTree.column_list in
+ let col = columns#add column_type in
+ let vcol = GTree.view_column ~title:column_title ()
+ ~renderer:(GTree.cell_renderer_text[], ["text",col]) in
+ let store = GTree.list_store columns in
+ object(self)
+ inherit GTree.view obj
+ method clear = store#clear
+ method append (v : 'a) =
+ let row = store#append () in
+ store#set ~row ~column:col v;
+ method column = col
+ initializer
+ self#set_model (Some (store :> GTree.model)) ;
+ ignore (self#append_column vcol)
+ end
+
+ class ['a,'b] twoColumnsCList gtree_view ~column1_type ~column2_type
+ ~column1_title ~column2_title
+ =
+ let obj =
+ ((Gobject.unsafe_cast gtree_view#as_widget) : Gtk.tree_view Gtk.obj) in
+ let columns = new GTree.column_list in
+ let col1 = columns#add column1_type in
+ let vcol1 = GTree.view_column ~title:column1_title ()
+ ~renderer:(GTree.cell_renderer_text[], ["text",col1]) in
+ let col2 = columns#add column2_type in
+ let vcol2 = GTree.view_column ~title:column2_title ()
+ ~renderer:(GTree.cell_renderer_text[], ["text",col2]) in
+ let store = GTree.list_store columns in
+ object(self)
+ inherit GTree.view obj
+ method clear = store#clear
+ method append (v1 : 'a) (v2 : 'b) =
+ let row = store#append () in
+ store#set ~row ~column:col1 v1;
+ store#set ~row ~column:col2 v2
+ method column1 = col1
+ method column2 = col2
+ initializer
+ self#set_model (Some (store :> GTree.model)) ;
+ ignore (self#append_column vcol1) ;
+ ignore (self#append_column vcol2) ;
+ end
+
+ class subscribeWindow () =
+ object(self)
+ inherit Hbugs_client_gui.subscribeWindow ()
+ val mutable tutorsSmartCList = None
+ method tutorsSmartCList =
+ match tutorsSmartCList with
+ None -> assert false
+ | Some w -> w
+ initializer
+ tutorsSmartCList <-
+ Some
+ (new twoColumnsCList self#tutorsCList
+ ~column1_type:Gobject.Data.string ~column2_type:Gobject.Data.string
+ ~column1_title:"Id" ~column2_title:"Description")
+ end
+
+ class hbugsMainWindow () =
+ object(self)
+ inherit Hbugs_client_gui.hbugsMainWindow ()
+ val mutable subscriptionSmartCList = None
+ val mutable hintsSmartCList = None
+ method subscriptionSmartCList =
+ match subscriptionSmartCList with
+ None -> assert false
+ | Some w -> w
+ method hintsSmartCList =
+ match hintsSmartCList with
+ None -> assert false
+ | Some w -> w
+ initializer
+ subscriptionSmartCList <-
+ Some
+ (new oneColumnCList self#subscriptionCList
+ ~column_type:Gobject.Data.string ~column_title:"Description")
+ initializer
+ hintsSmartCList <-
+ Some
+ (new oneColumnCList self#hintsCList
+ ~column_type:Gobject.Data.string ~column_title:"Description")
+ end
+
+ end
+;;
+
+class hbugsClient
+ ?(use_hint_callback: hint -> unit = do_nothing)
+ ?(describe_hint_callback: hint -> unit = do_nothing)
+ ?(destroy_callback: unit -> unit = do_nothing)
+ ()
+ =
+
+ let http_url_RE = Pcre.regexp "^(http://)?(.*):(\\d+)" in
+ let port_of_http_url url =
+ try
+ let subs = Pcre.extract ~rex:http_url_RE url in
+ int_of_string subs.(3)
+ with e -> raise (Invalid_URL url)
+ in
+
+ object (self)
+
+ val mainWindow = new SmartHbugs_client_gui.hbugsMainWindow ()
+ val subscribeWindow = new SmartHbugs_client_gui.subscribeWindow ()
+ val messageDialog = new Hbugs_client_gui.messageDialog ()
+ val myOwnId = Hbugs_id_generator.new_client_id ()
+ val mutable use_hint_callback = use_hint_callback
+ val mutable myOwnUrl = "localhost:49082"
+ val mutable brokerUrl = "localhost:49081"
+ val mutable brokerId: broker_id option = None
+ (* all available tutors, saved last time a List_tutors message was sent to
+ broker *)
+ val mutable availableTutors: tutor_dsc list = []
+ val mutable statusContext = None
+ val mutable subscribeWindowStatusContext = None
+ val mutable debug = false (* enable/disable debugging buttons *)
+ val mutable hints = [] (* actually available hints *)
+
+ initializer
+ self#initGui;
+ self#startLocalHttpDaemon ();
+ self#testLocalHttpDaemon ();
+ self#testBroker ();
+ self#registerToBroker ();
+ self#reconfigDebuggingButtons
+
+ method show = mainWindow#hbugsMainWindow#show
+ method hide = mainWindow#hbugsMainWindow#misc#hide
+
+ method setUseHintCallback callback =
+ use_hint_callback <- callback
+
+ method private debugButtons =
+ List.map
+ (fun (b: GButton.button) -> new GObj.misc_ops b#as_widget)
+ [ mainWindow#startLocalHttpDaemonButton;
+ mainWindow#testLocalHttpDaemonButton; mainWindow#testBrokerButton ]
+
+ method private initGui =
+
+ (* GUI: main window *)
+
+ (* ignore delete events so that hbugs window is closable only using
+ menu; on destroy (e.g. while quitting gTopLevel) self#quit is invoked
+ *)
+
+ ignore (mainWindow#hbugsMainWindow#event#connect#delete (fun _ -> true));
+ ignore (mainWindow#hbugsMainWindow#event#connect#destroy
+ (fun _ -> self#quit (); false));
+
+ (* GUI main window's menu *)
+ mainWindow#toggleDebuggingMenuItem#set_active debug;
+ ignore (mainWindow#toggleDebuggingMenuItem#connect#toggled
+ self#toggleDebug);
+
+ (* GUI: local HTTP daemon settings *)
+ ignore (mainWindow#clientUrlEntry#connect#changed
+ (fun _ -> myOwnUrl <- mainWindow#clientUrlEntry#text));
+ mainWindow#clientUrlEntry#set_text myOwnUrl;
+ ignore (mainWindow#startLocalHttpDaemonButton#connect#clicked
+ self#startLocalHttpDaemon);
+ ignore (mainWindow#testLocalHttpDaemonButton#connect#clicked
+ self#testLocalHttpDaemon);
+
+ (* GUI: broker choice *)
+ ignore (mainWindow#brokerUrlEntry#connect#changed
+ (fun _ -> brokerUrl <- mainWindow#brokerUrlEntry#text));
+ mainWindow#brokerUrlEntry#set_text brokerUrl;
+ ignore (mainWindow#testBrokerButton#connect#clicked self#testBroker);
+ mainWindow#clientIdLabel#set_text myOwnId;
+
+ (* GUI: client registration *)
+ ignore (mainWindow#registerClientButton#connect#clicked
+ self#registerToBroker);
+
+ (* GUI: subscriptions *)
+ ignore (mainWindow#showSubscriptionWindowButton#connect#clicked
+ (fun () ->
+ self#listTutors ();
+ subscribeWindow#subscribeWindow#show ()));
+
+ let get_selected_row_index () =
+ match mainWindow#hintsCList#selection#get_selected_rows with
+ [path] ->
+ (match GTree.Path.get_indices path with
+ [|n|] -> n
+ | _ -> assert false)
+ | _ -> assert false
+ in
+ (* GUI: hints list *)
+ ignore (
+ let event_ops = new GObj.event_ops mainWindow#hintsCList#as_widget in
+ event_ops#connect#button_press
+ (fun event ->
+ if GdkEvent.get_type event = `TWO_BUTTON_PRESS then
+ use_hint_callback (self#hint (get_selected_row_index ())) ;
+ false));
+
+ ignore (mainWindow#hintsCList#selection#connect#changed
+ (fun () ->
+ describe_hint_callback (self#hint (get_selected_row_index ())))) ;
+
+ (* GUI: main status bar *)
+ let ctxt = mainWindow#mainWindowStatusBar#new_context "0" in
+ statusContext <- Some ctxt;
+ ignore (ctxt#push "Ready");
+
+ (* GUI: subscription window *)
+ subscribeWindow#tutorsCList#selection#set_mode `MULTIPLE;
+ ignore (subscribeWindow#subscribeWindow#event#connect#delete
+ (fun _ -> subscribeWindow#subscribeWindow#misc#hide (); true));
+ ignore (subscribeWindow#listTutorsButton#connect#clicked self#listTutors);
+ ignore (subscribeWindow#subscribeButton#connect#clicked
+ self#subscribeSelected);
+ ignore (subscribeWindow#subscribeAllButton#connect#clicked
+ self#subscribeAll);
+ (subscribeWindow#tutorsCList#get_column 0)#set_visible false;
+ let ctxt = subscribeWindow#subscribeWindowStatusBar#new_context "0" in
+ subscribeWindowStatusContext <- Some ctxt;
+ ignore (ctxt#push "Ready");
+
+ (* GUI: message dialog *)
+ ignore (messageDialog#messageDialog#event#connect#delete
+ (fun _ -> messageDialog#messageDialog#misc#hide (); true));
+ ignore (messageDialog#okDialogButton#connect#clicked
+ (fun _ -> messageDialog#messageDialog#misc#hide ()))
+
+ (* accessory methods *)
+
+ (** pop up a (modal) dialog window showing msg to the user *)
+ method private showDialog msg =
+ messageDialog#dialogLabel#set_text msg;
+ messageDialog#messageDialog#show ()
+ (** use showDialog to display an hbugs message to the user *)
+ method private showMsgInDialog msg =
+ self#showDialog (Hbugs_messages.string_of_msg msg)
+
+ (** create a new thread which sends msg to broker, wait for an answer and
+ invoke callback passing response message as argument *)
+ method private sendReq ?(wait = false) ~msg callback =
+ let thread () =
+ try
+ callback (Hbugs_messages.submit_req ~url:(brokerUrl ^ "/act") msg)
+ with
+ | (Hbugs_messages.Parse_error (subj, reason)) as e ->
+ self#showDialog
+ (sprintf
+"Parse_error, unable to fullfill request. Details follow.
+Request: %s
+Error: %s"
+ (Hbugs_messages.string_of_msg msg) (Printexc.to_string e));
+ | (Unix.Unix_error _) as e ->
+ self#showDialog
+ (sprintf
+"Can't connect to HBugs Broker
+Url: %s
+Error: %s"
+ brokerUrl (Printexc.to_string e))
+ | e ->
+ self#showDialog
+ (sprintf "hbugsClient#sendReq: Uncaught exception: %s"
+ (Printexc.to_string e))
+ in
+ let th = Thread.create thread () in
+ if wait then
+ Thread.join th
+ else ()
+
+ (** check if a broker is authenticated using its broker_id
+ [ Background: during client registration, client save broker_id of its
+ broker, further messages from broker are accepted only if they carry the
+ same broker id ] *)
+ method private isAuthenticated id =
+ match brokerId with
+ | None -> false
+ | Some broker_id -> (id = broker_id)
+
+ (* actions *)
+
+ method private startLocalHttpDaemon =
+ (* flatten an hint tree to an hint list *)
+ let rec flatten_hint = function
+ | Hints hints -> List.concat (List.map flatten_hint hints)
+ | hint -> [hint]
+ in
+ fun () ->
+ let callback req outchan =
+ try
+ (match Hbugs_messages.msg_of_string req#body with
+ | Help ->
+ Hbugs_messages.respond_msg
+ (Usage "Local Http Daemon up and running!") outchan
+ | Hint (broker_id, hint) ->
+ if self#isAuthenticated broker_id then begin
+ let received_hints = flatten_hint hint in
+ List.iter
+ (fun h ->
+ (match h with Hints _ -> assert false | _ -> ());
+ ignore(mainWindow#hintsSmartCList#append(string_of_hint h)))
+ received_hints;
+ hints <- hints @ received_hints;
+ Hbugs_messages.respond_msg (Wow myOwnId) outchan
+ end else (* msg from unauthorized broker *)
+ Hbugs_messages.respond_exc "forbidden" broker_id outchan
+ | msg ->
+ Hbugs_messages.respond_exc
+ "unexpected_msg" (Hbugs_messages.string_of_msg msg) outchan)
+ with (Hbugs_messages.Parse_error _) as e ->
+ Hbugs_messages.respond_exc
+ "parse_error" (Printexc.to_string e) outchan
+ in
+ let addr = "0.0.0.0" in (* TODO actually user specified "My URL" is used
+ only as a value to be sent to broker, local HTTP
+ daemon will listen on "0.0.0.0", port is parsed
+ from My URL though *)
+ let httpDaemonThread () =
+ try
+ Http_daemon.start'
+ ~addr ~port:(port_of_http_url myOwnUrl) ~mode:`Single callback
+ with
+ | Invalid_URL url -> self#showDialog (sprintf "Invalid URL: \"%s\"" url)
+ | e ->
+ self#showDialog (sprintf "Can't start local HTTP daemon: %s"
+ (Printexc.to_string e))
+ in
+ ignore (Thread.create httpDaemonThread ())
+
+ method private testLocalHttpDaemon () =
+ try
+ let msg =
+ Hbugs_misc.http_post ~body:(Hbugs_messages.string_of_msg Help)
+ myOwnUrl
+ in
+ ignore msg
+(* self#showDialog msg *)
+ with
+ | Hbugs_misc.Malformed_URL url ->
+ self#showDialog
+ (sprintf
+ "Handshake with local HTTP daemon failed, Invalid URL: \"%s\""
+ url)
+ | Hbugs_misc.Malformed_HTTP_response res ->
+ self#showDialog
+ (sprintf
+ "Handshake with local HTTP daemon failed, can't parse HTTP response: \"%s\""
+ res)
+ | (Unix.Unix_error _) as e ->
+ self#showDialog
+ (sprintf
+ "Handshake with local HTTP daemon failed, can't connect: \"%s\""
+ (Printexc.to_string e))
+
+ method private testBroker () =
+ self#sendReq ~msg:Help
+ (function
+ | Usage _ -> ()
+ | unexpected_msg ->
+ self#showDialog
+ (sprintf
+ "Handshake with HBugs Broker failed, unexpected message:\n%s"
+ (Hbugs_messages.string_of_msg unexpected_msg)))
+
+ method registerToBroker () =
+ (match brokerId with (* undo previous registration, if any *)
+ | Some id -> self#unregisterFromBroker ()
+ | _ -> ());
+ self#sendReq ~msg:(Register_client (myOwnId, myOwnUrl))
+ (function
+ | Client_registered broker_id -> (brokerId <- Some broker_id)
+ | unexpected_msg ->
+ self#showDialog
+ (sprintf "Client NOT registered, unexpected message:\n%s"
+ (Hbugs_messages.string_of_msg unexpected_msg)))
+
+ method unregisterFromBroker () =
+ self#sendReq ~wait:true ~msg:(Unregister_client myOwnId)
+ (function
+ | Client_unregistered _ -> (brokerId <- None)
+ | unexpected_msg -> ())
+(*
+ self#showDialog
+ (sprintf "Client NOT unregistered, unexpected message:\n%s"
+ (Hbugs_messages.string_of_msg unexpected_msg)))
+*)
+
+ method stateChange new_state =
+ mainWindow#hintsSmartCList#clear ();
+ hints <- [];
+ self#sendReq
+ ~msg:(State_change (myOwnId, new_state))
+ (function
+ | State_accepted _ -> ()
+ | unexpected_msg ->
+ self#showDialog
+ (sprintf "State NOT accepted by Hbugs, unexpected message:\n%s"
+ (Hbugs_messages.string_of_msg unexpected_msg)))
+
+ method hint = List.nth hints
+
+ method private listTutors () =
+ (* wait is set to true just to make sure that after invoking listTutors
+ "availableTutors" is correctly filled *)
+ self#sendReq ~wait:true ~msg:(List_tutors myOwnId)
+ (function
+ | Tutor_list (_, descriptions) ->
+ availableTutors <- (* sort accordingly to tutor description *)
+ List.sort (fun (a,b) (c,d) -> compare (b,a) (d,c)) descriptions;
+ subscribeWindow#tutorsSmartCList#clear ();
+ List.iter
+ (fun (id, dsc) ->
+ ignore (subscribeWindow#tutorsSmartCList#append id dsc))
+ availableTutors
+ | unexpected_msg ->
+ self#showDialog
+ (sprintf "Can't list tutors, unexpected message:\n%s"
+ (Hbugs_messages.string_of_msg unexpected_msg)))
+
+ (* low level used by subscribeSelected and subscribeAll *)
+ method private subscribe' tutors_id =
+ self#sendReq ~msg:(Subscribe (myOwnId, tutors_id))
+ (function
+ | (Subscribed (_, subscribedTutors)) as msg ->
+ let sort = List.sort compare in
+ mainWindow#subscriptionSmartCList#clear ();
+ List.iter
+ (fun tutor_id ->
+ ignore
+ (mainWindow#subscriptionSmartCList#append
+ ( try
+ List.assoc tutor_id availableTutors
+ with Not_found -> assert false )))
+ tutors_id;
+ subscribeWindow#subscribeWindow#misc#hide ();
+ if sort subscribedTutors <> sort tutors_id then
+ self#showDialog
+ (sprintf "Subscription mismatch\n: %s"
+ (Hbugs_messages.string_of_msg msg))
+ | unexpected_msg ->
+ mainWindow#subscriptionSmartCList#clear ();
+ self#showDialog
+ (sprintf "Subscription FAILED, unexpected message:\n%s"
+ (Hbugs_messages.string_of_msg unexpected_msg)))
+
+ method private subscribeSelected () =
+ let tutorsSmartCList = subscribeWindow#tutorsSmartCList in
+ let selectedTutors =
+ List.map
+ (fun p ->
+ tutorsSmartCList#model#get
+ ~row:(tutorsSmartCList#model#get_iter p)
+ ~column:tutorsSmartCList#column1)
+ tutorsSmartCList#selection#get_selected_rows
+ in
+ self#subscribe' selectedTutors
+
+ method subscribeAll () =
+ self#listTutors (); (* this fills 'availableTutors' field *)
+ self#subscribe' (List.map fst availableTutors)
+
+ method private quit () =
+ self#unregisterFromBroker ();
+ destroy_callback ()
+
+ (** enable/disable debugging *)
+ method private setDebug value = debug <- value
+
+ method private reconfigDebuggingButtons =
+ List.iter (* debug value changed, reconfigure buttons *)
+ (fun (b: GObj.misc_ops) -> if debug then b#show () else b#hide ())
+ self#debugButtons;
+
+ method private toggleDebug () =
+ self#setDebug (not debug);
+ self#reconfigDebuggingButtons
+
+ end
+;;
+