OSDN Git Service

9c3a7df294622bed682fff7a7646818fa6a01974
[mingw/mingw-get.git] / src / guixmld.cpp
1 /*
2  * guixmld.cpp
3  *
4  * $Id$
5  *
6  * Written by Keith Marshall <keithmarshall@users.sourceforge.net>
7  * Copyright (C) 2012, MinGW.org Project
8  *
9  *
10  * Implementation of XML data loading services for the mingw-get GUI.
11  *
12  *
13  * This is free software.  Permission is granted to copy, modify and
14  * redistribute this software, under the provisions of the GNU General
15  * Public License, Version 3, (or, at your option, any later version),
16  * as published by the Free Software Foundation; see the file COPYING
17  * for licensing details.
18  *
19  * Note, in particular, that this software is provided "as is", in the
20  * hope that it may prove useful, but WITHOUT WARRANTY OF ANY KIND; not
21  * even an implied WARRANTY OF MERCHANTABILITY, nor of FITNESS FOR ANY
22  * PARTICULAR PURPOSE.  Under no circumstances will the author, or the
23  * MinGW Project, accept liability for any damages, however caused,
24  * arising from the use of this software.
25  *
26  */
27 #include "guimain.h"
28 #include "pkgbase.h"
29 #include "pkginet.h"
30 #include "pkgkeys.h"
31 #include "pkglist.h"
32 #include "pkgtask.h"
33
34 #include <unistd.h>
35 #include <wtkexcept.h>
36
37 /* The DMH sub-system provides the following function, but it does not
38  * declare the prototype; (this is to obviate any automatic requirement
39  * for every DMH client to include windows.h).  We have already included
40  * windows.h indirectly, via guimain.h, so it is convenient for us to
41  * declare the requisite prototype here.
42  */
43 EXTERN_C void dmh_setpty( HWND );
44
45 class ProgressMeterMaker: public pkgProgressMeter
46 {
47   /* A locally defined class, supporting progress metering
48    * for package catalogue update and load operations.
49    */
50   public:
51     ProgressMeterMaker( HWND, HWND, AppWindowMaker * );
52
53     virtual int Annotate( const char *, ... );
54     virtual void SetRange( int, int );
55     virtual void SetValue( int );
56
57   protected:
58     HWND message_line, progress_bar;
59 };
60
61 inline
62 pkgProgressMeter *AppWindowMaker::AttachProgressMeter( pkgProgressMeter *meter )
63 {
64   /* A local helper method for attaching a progress meter to the
65    * controlling class instance for the main application window.
66    */
67   if( AttachedProgressMeter == NULL )
68     AttachedProgressMeter = meter;
69   return AttachedProgressMeter;
70 }
71
72 inline void AppWindowMaker::DetachProgressMeter( pkgProgressMeter *meter )
73 {
74   /* A local helper method for detaching a progress meter from the
75    * controlling class instance for the main application window.
76    */
77   if( meter == AttachedProgressMeter )
78   {
79     pkgData->DetachProgressMeter( meter );
80     AttachedProgressMeter = NULL;
81   }
82 }
83
84 /* We need to provide a destructor for the abstract base class, from which
85  * our progress meters are derived; here is as good a place as any.
86  */
87 pkgProgressMeter::~pkgProgressMeter(){ referrer->DetachProgressMeter( this ); }
88
89 /* We must also provide the implementation of our local progress meter class.
90  */
91 ProgressMeterMaker::ProgressMeterMaker
92 ( HWND annotation, HWND indicator, AppWindowMaker *owner ):
93 pkgProgressMeter( owner ), message_line( annotation ), progress_bar( indicator )
94 {
95   /* Constructor creates an instance of the progress meter class, attaching it
96    * to the main application window, with an initial metering range of 0..100%,
97    * and a starting indicated completion state of 0%.
98    */
99   owner->AttachProgressMeter( this );
100   SetRange( 0, 100 );
101   SetValue( 0 );
102 }
103
104 int ProgressMeterMaker::Annotate( const char *fmt, ... )
105 {
106   /* Method to add a printf() style annotation to the progress meter dialogue.
107    */
108   va_list argv;
109   va_start( argv, fmt );
110   char annotation[1 + vsnprintf( NULL, 0, fmt, argv )];
111   int len = vsnprintf( annotation, sizeof( annotation ), fmt, argv );
112   va_end( argv );
113
114   SendMessage( message_line, WM_SETTEXT, 0, (LPARAM)(annotation) );
115   return len;
116 }
117
118 void ProgressMeterMaker::SetRange( int min, int max )
119 {
120   /* Method to adjust the range of the progress meter, to represent any
121    * arbitrary range of discrete values, rather than percentage units.
122    */
123   SendMessage( progress_bar, PBM_SETRANGE, 0, MAKELPARAM( min, max ) );
124 }
125
126 void ProgressMeterMaker::SetValue( int value )
127 {
128   /* Method to update the indicated completion state of a progress meter,
129    * to represent any arbitrary value within its assigned metering range.
130    */
131   SendMessage( progress_bar, PBM_SETPOS, value, 0 );
132 }
133
134 /* Implementation of service routines, for loading the package catalogue
135  * from its defining collection of XML files.
136  */
137 void AppWindowMaker::LoadPackageData( bool force_update )
138 {
139   /* Helper method to load the package database from its
140    * defining collection of XML catalogue files.
141    */
142   const char *dfile;
143   if( pkgData == NULL )
144   {
145     /* This is the first request to load the database;
146      * establish the load starting point as "profile.xml",
147      * if available...
148      */
149     if( access( dfile = xmlfile( profile_key ), R_OK ) != 0 )
150     {
151       /* ...or as "defaults.xml" otherwise.
152        */
153       free( (void *)(dfile) );
154       dfile = xmlfile( defaults_key );
155     }
156   }
157   else
158   { /* This is a reload request; in this case we adopt the
159      * starting point as established for the initial load...
160      */
161     dfile = strdup( pkgData->Value() );
162     /*
163      * ...and clear out all stale data from the previous
164      * time of loading.
165      */
166     delete pkgData;
167   }
168
169   /* Commence loading...
170    */
171   if( ! (pkgData = new pkgXmlDocument( dfile ))->IsOk() )
172     /*
173      * ...bailing out on failure to access the initial file.
174      */
175     throw WTK::runtime_error( WTK::error_text(
176         "%s: cannot open package database", dfile
177       ));
178
179   /* Once the initial file has been loaded, its name is
180    * recorded within the XML data image itself; thus, we
181    * may release the heap memory used to establish it
182    * prior to opening the file.
183    */
184   free( (void *)(dfile) );
185
186   /* Establish the repository URI references, for retrieval
187    * of the downloadable catalogue files, and load them...
188    */
189   pkgData->AttachProgressMeter( AttachedProgressMeter );
190   if( pkgData->BindRepositories( force_update ) == NULL )
191     /*
192      * ...once again, bailing out on failure.
193      */
194     throw WTK::runtime_error( "Cannot read package catalogue" );
195
196   /* Finally, load the installation records pertaining to
197    * the active system map.
198    */
199   pkgData->LoadSystemMap();
200 }
201
202 static void pkgInvokeInitDataLoad( void *window )
203 {
204   /* Thread procedure for performing the initial catalogue load, on
205    * application start-up.  This will load from locally cached data
206    * files, when available; however, it will also initiate a download
207    * from the remote repository, for any file which is missing from
208    * the local cache.  Since this may be a time consuming process,
209    * we subject it to progress metering, to ensure that the user is
210    * not left staring at an apparently hung, blank window.
211    */
212   HWND msg = GetDlgItem( (HWND)(window), IDD_PROGRESS_MSG );
213   HWND dlg = GetDlgItem( (HWND)(window), IDD_PROGRESS_BAR );
214   AppWindowMaker *app = GetAppWindow( GetParent( (HWND)(window) ));
215   SendMessage( (HWND)(window),
216       WM_SETTEXT, 0, (LPARAM)("Loading Package Catalogue")
217     );
218   ProgressMeterMaker ui( msg, dlg, app );
219
220   /* For this activity, we request automatic dismissal of the dialogue,
221    * when loading has been completed; the user will have an opportunity
222    * to countermand this choice, if loading is delayed by the required
223    * download of any missing local catalogue file.
224    */
225   dlg = GetDlgItem( (HWND)(window), IDD_AUTO_CLOSE_OPTION );
226   SendMessage( dlg, WM_SETTEXT, 0,
227       (LPARAM)("Close dialogue automatically, when loading is complete.")
228     );
229   CheckDlgButton( (HWND)(window), IDD_AUTO_CLOSE_OPTION, BST_CHECKED );
230
231   /* We've now set up the initial state for the progress meter dialogue;
232    * proceed to load, (and perhaps download), the XML data files.
233    */
234   app->LoadPackageData( false );
235
236   /* When loading has been completed, automatically dismiss the dialogue...
237    */
238   if( IsDlgButtonChecked( (HWND)(window), IDD_AUTO_CLOSE_OPTION ) )
239     SendMessage( (HWND)(window), WM_COMMAND, (WPARAM)(IDOK), 0 );
240
241   /* ...unless the user has countermanded the automatic dismissal request...
242    */
243   else
244   { /* ...in which case, we activate the manual dismissal button...
245      */
246     if( (dlg = GetDlgItem( (HWND)(window), IDOK )) != NULL )
247       EnableWindow( dlg, TRUE );
248
249     /* ...and notify the user that it must be clicked to continue.
250      */
251     ui.Annotate( "Data has been loaded; please close this dialogue to continue." );
252   }
253 }
254
255 static void pkgInvokeUpdate( void *window )
256 {
257   /* Thread procedure for performing a package catalogue update.
258    * This will download catalogue files from the remote repository,
259    * and integrate them into the locally cached catalogue XML file
260    * set.  Since this is normally a time consuming process, we must
261    * subject it to progress metering, to ensure that the user is
262    * not left staring at an apparently hung, blank window.
263    */
264   HWND msg = GetDlgItem( (HWND)(window), IDD_PROGRESS_MSG );
265   HWND dlg = GetDlgItem( (HWND)(window), IDD_PROGRESS_BAR );
266   AppWindowMaker *app = GetAppWindow( GetParent( (HWND)(window) ));
267   ProgressMeterMaker ui( msg, dlg, app );
268
269   /* After setting up the progress meter, we clear out any data
270    * which was previously loaded into the package list, reload it
271    * with the "forced download" option, and refresh the display.
272    */
273   app->ClearPackageList();
274   app->LoadPackageData( true );
275   app->UpdatePackageList();
276
277   /* During the update, the user may have selected the option for
278    * automatic dismissal of the dialogue box on completion...
279    */
280   if( IsDlgButtonChecked( (HWND)(window), IDD_AUTO_CLOSE_OPTION ) )
281     /*
282      * ...in which case, we dismiss it without further ado...
283      */
284     SendMessage( (HWND)(window), WM_COMMAND, (WPARAM)(IDOK), 0 );
285
286   else
287   { /* ...otherwise, we activate the manual dismissal button...
288      */
289     if( (dlg = GetDlgItem( (HWND)(window), IDOK )) != NULL )
290       EnableWindow( dlg, TRUE );
291
292     /* ...and notify the user that it must be clicked to continue.
293      */
294     ui.Annotate( "Update is complete; please close this dialogue to continue." );
295   }
296 }
297
298 static int CALLBACK pkgDialogue
299 ( HWND window, unsigned int msg, WPARAM wParam, LPARAM lParam )
300 {
301   /* Generic handler for dialogue boxes which delegate an associated
302    * processing activity to a background thread.
303    */
304   switch( msg )
305   {
306     /* We need to handle only two classes of windows messages
307      * on behalf of such dialogue boxes...
308      */
309     case WM_INITDIALOG:
310       /* ...viz. on initial dialogue box creation, we delegate the
311        * designated activity to the background thread...
312        */
313       _beginthread( AppWindowMaker::DialogueThread, 0, (void *)(window) );
314       return TRUE;
315
316     case WM_COMMAND:
317       if( LOWORD( wParam ) == IDOK )
318       {
319         /* ...then we wait for a notification that the dialogue may be
320          * closed, (which isn't permitted until the thread completes).
321          */
322         EndDialog( window, 0 );
323         return TRUE;
324       }
325   }
326   /* Any other messages, which are directed to this dialogue box,
327    * may be safely ignored.
328    */
329   return FALSE;
330 }
331
332 /* The following static member of the AppWindowMaker class is used
333  * to pass the function reference for the worker thread process to
334  * the preceding dialogue box handler function, when it is invoked
335  * by the following helper method.
336  */
337 pkgDialogueThread *AppWindowMaker::DialogueThread = NULL;
338
339 int AppWindowMaker::DispatchDialogueThread( int id, pkgDialogueThread *handler )
340 {
341   /* Helper method to open a dialogue box, and to initiate a worker
342    * thread to handle a designated background process on its behalf.
343    */
344   DialogueThread = handler;
345   return DialogueResponse( id, pkgDialogue );
346 }
347
348 inline unsigned long AppWindowMaker::EnumerateActions( int classified )
349 {
350   /* Helper method to enumerate the actions of a specified
351    * class, within the current schedule.
352    */
353   return pkgData->Schedule()->EnumeratePendingActions( classified );
354 }
355
356 pkgActionItem *pkgActionItem::Clear( pkgActionItem *schedule, unsigned long mask )
357 # define ACTION_PRESERVE_FAILED  (ACTION_DOWNLOAD_FAILED | ACTION_APPLY_FAILED)
358 {
359   /* Method to remove those action items which have no attribute flags in common
360    * with the specified mask, from the schedule; return the residual schedule of
361    * items, if any, which were not removed.  (Note that specifying a mask with a
362    * value of 0UL, which is the default, results in removal of all items).
363    */
364   pkgActionItem *residual = NULL;
365
366   /* Starting at the specified item, or the invoking class object item
367    * if no starting point is specified...
368    */
369   if( (schedule != NULL) || ((schedule = this) != NULL) )
370   {
371     /* ...and provided this starting point is not NULL, walk back to
372      * the first item in the associated task schedule...
373      */
374     while( schedule->prev != NULL ) schedule = schedule->prev;
375     while( schedule != NULL )
376     {
377       /* ...then, processing each scheduled task item in sequence, and
378        * keeping track of the next to be processed...
379        */
380       pkgActionItem *nextptr = schedule->next;
381       if( (schedule->flags & mask) == 0 )
382         /*
383          * ...delete each which doesn't match any masked attribute...
384          */
385         delete schedule;
386
387       else
388         /* ...otherwise add it to the residual schedule.
389          */
390         residual = schedule;
391
392       /* In either event, move on to the next item in sequence, if any.
393        */
394       schedule = nextptr;
395     }
396   }
397   /* Ultimately, return a pointer to the last item added to the residual
398    * schedule, or NULL if all items were deleted.
399    */
400   return residual;
401 }
402
403 void pkgActionItem::Reset
404 ( unsigned long set, unsigned long mask, pkgActionItem *schedule )
405 {
406   /* A method to manipulate the control, error trapping, and state
407    * flags for all items in the specified schedule of actions.
408    *
409    * Starting at the specified item, or the invoking class object
410    * item if no starting point is specified...
411    */
412   if( (schedule != NULL) || ((schedule = this) != NULL) )
413   {
414     /* ...and provided this starting point is not NULL, walk back
415      * to the first item in the associated task schedule...
416      */
417     while( schedule->prev != NULL ) schedule = schedule->prev;
418     while( schedule != NULL )
419     {
420       /* ...then, processing each scheduled task item in sequence,
421        * update the flags according to the specified mask and new
422        * bits to be set...
423        */
424       schedule->flags = (flags & mask) | set;
425       /*
426        * ...before moving on to the next item in the sequence.
427        */
428       schedule = schedule->next;
429     }
430   }
431 }
432
433 static int pkgActionCount( HWND dlg, int id, const char *fmt, int classified )
434 {
435   /* Helper function to itemise the currently scheduled actions
436    * of a specified class, recording the associated package name
437    * within the passed EDITTEXT control, and noting the count of
438    * itemised actions within the heading of the control.
439    *
440    * First, count the actions, while adding the package names
441    * to the edit control with the specified ID.
442    */
443   dmh_setpty( GetDlgItem( dlg, id ) );
444   int count = GetAppWindow( GetParent( dlg ))->EnumerateActions( classified );
445
446   /* Construct the heading, according to the specified format,
447    * and including the identified action count.
448    */
449   const char *packages = (count == 1) ? "package" : "packages";
450   char label_text[1 + snprintf( NULL, 0, fmt, count, packages )];
451   snprintf( label_text, sizeof( label_text), fmt, count, packages );
452
453   /* Finally, update the heading on the edit control, assigning
454    * it to the dialogue control with ID one greater than that of
455    * the edit control itself.
456    */
457   SendMessage( GetDlgItem( dlg, ++id ), WM_SETTEXT, 0, (LPARAM)(label_text) );
458   dmh_setpty( NULL );
459   return count;
460 }
461
462 /* Implement a macro as shorthand notation for passing of action
463  * specific argument lists to the pkgActionCount() function...
464  */
465 #define ACTION_APPLY(OP)    APPLIES_TO(OP), FMT_APPLY_##OP##S, ACTION_##OP
466 #define APPLIES_TO(OP)      IDD_APPLY_##OP##S_PACKAGES
467
468 /* ...using the appropriate selection from these action specific
469  * message formats.
470  */
471 #define FMT_APPLY_REMOVES   "%u installed %s will be removed"
472 #define FMT_APPLY_UPGRADES  "%u installed %s will be upgraded"
473 #define FMT_APPLY_INSTALLS  "%u new/upgraded %s will be installed"
474
475 static int CALLBACK pkgApplyApproved
476 ( HWND window, unsigned int msg, WPARAM wParam, LPARAM lParam )
477 {
478   /* Callback function servicing the custom dialogue box in which
479    * scheduled actions are itemised for user confirmation, prior
480    * to applying them.
481    */
482   switch( msg )
483   { case WM_INITDIALOG:
484       /* On opening the dialogue box, itemise each of the
485        * remove, upgrade, and install action classes.
486        */
487       pkgActionCount( window, ACTION_APPLY( REMOVE ) );
488       pkgActionCount( window, ACTION_APPLY( UPGRADE ) );
489       pkgActionCount( window, ACTION_APPLY( INSTALL ) );
490       return TRUE;
491
492     case WM_COMMAND:
493       /* Wait for the user to review the itemised schedule
494        * of pending changes, confirm intent to proceed...
495        */
496       long opt = LOWORD( wParam );
497       if( (opt == ID_APPLY) || (opt == ID_DISCARD) || (opt == ID_DEFER) )
498       {
499         /* ...then close the dialogue, passing the selected
500          * continuation option back to the caller.
501          */
502         EndDialog( window, opt );
503         return TRUE;
504       }
505   }
506   /* Ignore any window messages we don't recognise.
507    */
508   return FALSE;
509 }
510
511 int AppWindowMaker::Invoked( void )
512 {
513   /* Override for the WTK::MainWindowMaker::Invoked() method; it
514    * provides the hook for the initial loading of the XML database,
515    * and creation of the display controls through which its content
516    * will be presented to the user, prior to invocation of the main
517    * window's message loop.
518    *
519    * The data displays depend on the MS-Windows Common Controls API;
520    * initialise all components of this up front.
521    */
522   InitCommonControls();
523
524   /* Load the data from the XML catalogue files, and construct
525    * the initial view of the available package list; this activity
526    * is invoked in a background thread, initiated from a progress
527    * dialogue derived from the "Update Catalogue" template.
528    */
529   DispatchDialogueThread( IDD_REPO_UPDATE, pkgInvokeInitDataLoad );
530   InitPackageListView();
531
532   /* Initialise the data-sheet tab control, displaying the default
533    * "no package selected" message.
534    */
535   InitPackageTabControl();
536
537   /* Force a layout adjustment, to ensure that the displayed
538    * data controls are correctly populated.
539    */
540   AdjustLayout();
541
542   /* Finally, we may delegate all further processing to the main
543    * window's message loop.
544    */
545   return WTK::MainWindowMaker::Invoked();
546 }
547
548 long AppWindowMaker::OnCommand( WPARAM cmd )
549 {
550   /* Handler for WM_COMMAND messages which are directed to the
551    * top level application window.
552    */
553   switch( cmd )
554   { case IDM_HELP_ABOUT:
555       /* This request is initiated by selecting "About mingw-get"
556        * from the "Help" menu; we respond by displaying the "about"
557        * dialogue box.
558        */
559       WTK::GenericDialogue( AppInstance, AppWindow, IDD_HELP_ABOUT );
560       break;
561
562     case IDM_PACKAGE_INSTALL:
563       /* Initiated by selecting the "Mark for Installation" option
564        * from the "Package" menu, this request will schedule the
565        * currently selected package, and any currently unfulfilled
566        * dependencies, for installation.
567        */
568       Schedule( ACTION_INSTALL );
569       break;
570
571     case IDM_PACKAGE_UPGRADE:
572       /* Initiated by selecting the "Mark for Upgrade" option
573        * from the "Package" menu, this request will schedule the
574        * currently selected package, and any currently unfulfilled
575        * dependencies, for upgrade or installation, as appropriate.
576        */
577       Schedule( ACTION_UPGRADE );
578       break;
579
580     case IDM_PACKAGE_REMOVE:
581       /* Initiated by selecting the "Mark for Removal" option
582        * from the "Package" menu, this request will schedule the
583        * currently selected package for removal.
584        */
585       Schedule( ACTION_REMOVE );
586       break;
587
588     case IDM_PACKAGE_UNMARK:
589       /* Initiated by selecting the "Unmark" option from the
590        * "Package" menu, this request will cancel the effect of
591        * any previously scheduled action, in respect of the
592        * currently selected package.
593        */
594       UnmarkSelectedPackage();
595       break;
596
597     case IDM_REPO_UPDATE:
598       /* This request is initiated by selecting "Update Catalogue"
599        * from the "Repository" menu; we respond by initiating a progress
600        * dialogue, from which a background thread is invoked to download
601        * fresh copies of the package catalogue files from the remote
602        * repository, and consolidate them into the local catalogue.
603        */
604       DispatchDialogueThread( IDD_REPO_UPDATE, pkgInvokeUpdate );
605       break;
606
607     case IDM_REPO_APPLY:
608       /* Initiated when the user selects the "Apply Changes" option,
609        * we first reset the error trapping and download request state
610        * for all scheduled actions, then we present the user with a
611        * dialogue requesting confirmation of approval to proceed.
612        */
613       pkgData->Schedule()->Reset( ACTION_DOWNLOAD, ~ACTION_PRESERVE_FAILED );
614       switch( DialogueResponse( IDD_APPLY_APPROVE, pkgApplyApproved ) )
615       {
616         /* Of the three possible responses, we simply ignore the "Defer"
617          * option, (since it requires no action), but we must explicitly
618          * handle the "Apply" and "Discard" options.
619          */
620         case ID_APPLY:
621           /* When "Apply" confirmation is forthcoming, we proceed to
622            * download any required packages, and invoke the scheduled
623            * remove, upgrade, or install actions.
624            *
625            * FIXME: the pkgInvokeDownload() thread handler has yet to
626            * be implemented.
627            */
628           DispatchDialogueThread( IDD_APPLY_DOWNLOAD, pkgInvokeDownload );
629 //        DispatchDialogueThread( IDD_APPLY_MONITORED, pkgApplyChanges );
630
631           /* After applying changes, we fall through...
632            */
633         case ID_DISCARD:
634           /* ...so that on explicit "Discard" selection by the user,
635            * or following application of all scheduled actions, as a
636            * result of processing the "Apply" selection, we clear the
637            * actions schedule, remove all marker icons, and refresh
638            * the package list to reflect current status.
639            */
640           pkgListViewMaker pkglist( PackageListView );
641           pkglist.UpdateListView();
642           
643           /* Updating the list view clears pending action marks from
644            * every entry, but clearing the schedule may not cancel any
645            * request relating to a failed action; restore marked state
646            * for such residual actions.
647            */
648           if( pkgData->ClearScheduledActions( ACTION_PRESERVE_FAILED ) != NULL )
649             pkglist.MarkScheduledActions( pkgData->Schedule() );
650
651           /* Clearing the schedule of actions may also affect the
652            * validity of menu options; update accordingly.
653            */
654           UpdatePackageMenuBindings();
655       }
656       break;
657
658     case IDM_REPO_QUIT:
659       /* This request is initiated by selecting the "Quit" option
660        * from the "Repository" menu; we respond by sending a WM_CLOSE
661        * message, to terminate the current application instance.
662        */
663       SendMessage( AppWindow, WM_CLOSE, 0, 0L );
664       break;
665   }
666   /* Any other message is silently ignored.
667    */
668   return EXIT_SUCCESS;
669 }
670
671 /* $RCSfile$: end of file */