1 /**************************************************************************
2 Copyright:
3 (C) 2008 - 2012 Alexander Shaduri <ashaduri 'at' gmail.com>
4 License: See LICENSE_gsmartcontrol.txt
5 ***************************************************************************/
6 /// \file
7 /// \author Alexander Shaduri
8 /// \ingroup gsc
9 /// \weakgroup gsc
10 /// @{
11
12 #include <string>
13 // #include <locale.h> // _configthreadlocale (win32)
14 #include <stdexcept> // std::runtime_error
15 #include <cstdio> // std::printf
16 #include <vector>
17 #include <sstream>
18 #include <limits>
19 #include <gtkmm.h>
20 #include <glib.h> // g_, G*
21 #include <glibmm.h> // set_application_name
22
23 #ifdef _WIN32
24 #include <windows.h>
25 #include <versionhelpers.h>
26 #endif
27
28 #include "hz/hz_config.h" // ENABLE_GLIB, VERSION, DEBUG_BUILD
29
30 #include "libdebug/libdebug.h" // include full libdebug here (to add domains, etc...)
31 #include "rconfig/rconfig.h" // include full rconfig here
32 #include "hz/data_file.h" // data_file_add_search_directory
33 #include "hz/fs_tools.h" // get_user_config_dir()
34 #include "hz/fs_path.h" // FsPath
35 #include "hz/locale_tools.h" // locale_c*
36 #include "hz/string_algo.h" // string_join()
37 #include "hz/win32_tools.h" // win32_get_registry_value_string()
38 #include "hz/env_tools.h"
39
40 #include "gsc_main_window.h"
41 #include "gsc_executor_log_window.h"
42 #include "gsc_settings.h"
43 #include "gsc_init.h"
44
45
46
47 namespace {
48
49 /// Config file in user's HOME
50 static std::string s_home_config_file;
51
52
53 /// Libdebug channel buffer
54 static debug_channel_base_ptr s_debug_buf_channel;
55
56 /// Libdebug channel buffer stream
57 static std::ostringstream s_debug_buf_channel_stream;
58
59
60 /// Get libdebug buffer channel (create new one if unavailable).
61 /// This function is not thread-safe. The channel must be locked properly.
app_get_debug_buf_channel()62 inline debug_channel_base_ptr app_get_debug_buf_channel()
63 {
64 if (!s_debug_buf_channel)
65 return (s_debug_buf_channel = new DebugChannelOStream(s_debug_buf_channel_stream));
66 return s_debug_buf_channel;
67 }
68
69 }
70
71
72
app_get_debug_buffer_str()73 std::string app_get_debug_buffer_str()
74 {
75 debug_channel_base_ptr channel = app_get_debug_buf_channel();
76 DebugChannelOStream::intrusive_ptr_scoped_lock_type locker(channel->get_ref_mutex()); // lock
77 // now the channel is locked, so we have a proper access to the stream.
78 return s_debug_buf_channel_stream.str();
79 }
80
81
82
83
84
85 namespace {
86
87
88 /// Find the configuration files and load them.
app_init_config()89 inline bool app_init_config()
90 {
91 // $XDG_CONFIG_DIRS defaults to /etc/xdg, which is really silly.
92 // Actually, the whole specification is silly imho, since instead of
93 // removing just one directory/file I have to remove 3 now (and tell
94 // the users to do the same thing). And the *_DIRS stuff completely
95 // breaks parallel installations of the same program. Implementing
96 // only $XDG_CONFIG_HOME is harmless enough, so we do it.
97
98 s_home_config_file = hz::get_user_config_dir() + hz::DIR_SEPARATOR_S
99 + "gsmartcontrol" + hz::DIR_SEPARATOR_S + "gsmartcontrol.conf";
100
101 std::string global_config_file;
102 std::string old_local_config; // pre-XDG and pre-CSIDL_APPDATA file for migration
103
104 #ifdef _WIN32
105 global_config_file = "gsmartcontrol.conf"; // CWD, installation dir by default.
106
107 std::string old_config_dir;
108 hz::win32_get_registry_value_string(HKEY_CURRENT_USER,
109 "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", "Personal", old_config_dir);
110
111 old_local_config = old_config_dir + hz::DIR_SEPARATOR_S + "gsmartcontrol.conf";
112
113 #else
114 global_config_file = std::string(PACKAGE_SYSCONF_DIR)
115 + hz::DIR_SEPARATOR_S + "gsmartcontrol.conf";
116
117 old_local_config = hz::get_home_dir() + hz::DIR_SEPARATOR_S + ".gsmartcontrolrc";
118 #endif
119
120 debug_out_dump("app", DBG_FUNC_MSG << "Global config file: \"" << global_config_file << "\"\n");
121 debug_out_dump("app", DBG_FUNC_MSG << "Local config file: \"" << s_home_config_file << "\"\n");
122 debug_out_dump("app", DBG_FUNC_MSG << "Old local config file: \"" << old_local_config << "\"\n");
123
124 hz::FsPath gp(global_config_file); // Default system-wide settings. This file is empty by default.
125 hz::FsPath hp(s_home_config_file); // Per-user settings.
126 hz::FsPath op(old_local_config); // Old user settings, should be migrated.
127
128 if (gp.exists() && gp.is_readable()) { // load global first
129 rconfig::load_from_file(gp.str());
130 }
131
132 if (hp.exists() && hp.is_readable()) { // load local
133 rconfig::load_from_file(hp.str());
134
135 } else {
136 // create the parent directories of the config file
137 hz::FsPath config_loc(hp.get_dirname());
138
139 if (!config_loc.exists()) {
140 config_loc.make_dir(0700, true); // with parents.
141 }
142
143 if (op.exists() && op.is_readable()) { // load the old config if the new one doesn't exist.
144 debug_print_info("app", "Old configuration file found at \"%s\", migrating to \"%s\".\n", op.c_str(), hp.c_str());
145 rconfig::load_from_file(op.str());
146 // force saving of the config to the new location (so that it's preserved in case of crash).
147 if (rconfig::save_to_file(hp.str())) {
148 // remove the old config
149 op.remove();
150 }
151 }
152 }
153
154
155 rconfig::dump_tree();
156
157 init_default_settings(); // initialize /default
158
159 #if defined ENABLE_GLIB && ENABLE_GLIB
160 rconfig::autosave_set_config_file(s_home_config_file);
161 uint32_t autosave_timeout = rconfig::get_data<uint32_t>("system/config_autosave_timeout");
162 if (autosave_timeout)
163 rconfig::autosave_start(autosave_timeout);
164 #endif
165
166 return true;
167 }
168
169
170
171 /// If it's the first time the application was started by this user, show a message.
app_show_first_boot_message(Gtk::Window * parent)172 inline void app_show_first_boot_message(Gtk::Window* parent)
173 {
174 bool first_boot = false;
175 rconfig::get_data("system/first_boot", first_boot);
176
177 if (first_boot) {
178 // Glib::ustring msg = "First boot";
179 // Gtk::MessageDialog(*parent, msg, false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK, true).run();
180 }
181
182 // rconfig::set_data("system/first_boot", false); // don't show it again
183 }
184
185 } // anon. ns
186
187
188
189 extern "C" {
190 /// Glib message -> libdebug message convertor
glib_message_handler(const gchar * log_domain,GLogLevelFlags log_level,const gchar * message,gpointer user_data)191 static void glib_message_handler(const gchar* log_domain, GLogLevelFlags log_level,
192 const gchar* message, gpointer user_data)
193 {
194 // log_domain is already printed as part of message.
195 debug_print_error("gtk", "%s\n", message);
196 }
197 }
198
199
200
201
202 namespace {
203
204
205 /// Command-line argument values
206 struct CmdArgs {
CmdArgs__anon26e135bb0311::CmdArgs207 CmdArgs() :
208 // defaults
209 arg_locale(TRUE),
210 arg_version(FALSE),
211 arg_scan(TRUE),
212 arg_hide_tabs(TRUE),
213 arg_add_virtual(NULL),
214 arg_add_device(NULL),
215 arg_gdk_scale(std::numeric_limits<double>::quiet_NaN()),
216 arg_gdk_dpi_scale(std::numeric_limits<double>::quiet_NaN())
217 { }
218
219 // Note: Use GLib types here:
220 gboolean arg_locale; ///< if false, disable using system locale
221 gboolean arg_version; ///< if true, show version and exit
222 gboolean arg_scan; ///< if false, don't scan the system for drives on startup
223 gboolean arg_hide_tabs; ///< if true, hide additional info tabs when smart is disabled. false may help debugging.
224 gchar** arg_add_virtual; ///< load smartctl data from these files as virtual drives
225 gchar** arg_add_device; ///< add these device files manually
226 double arg_gdk_scale; ///< The value of GDK_SCALE environment variable
227 double arg_gdk_dpi_scale; ///< The value of GDK_DPI_SCALE environment variable
228 };
229
230
231
232 /// Parse command-line arguments (fills \c args)
parse_cmdline_args(CmdArgs & args,int & argc,char ** & argv)233 inline bool parse_cmdline_args(CmdArgs& args, int& argc, char**& argv)
234 {
235 static const GOptionEntry arg_entries[] =
236 {
237 { "no-locale", 'l', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &(args.arg_locale),
238 "Don't use system locale", NULL },
239 { "version", 'V', 0, G_OPTION_ARG_NONE, &(args.arg_version),
240 "Display version information", NULL },
241 { "no-scan", '\0', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &(args.arg_scan),
242 "Don't scan devices on startup", NULL },
243 { "no-hide-tabs", '\0', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &(args.arg_hide_tabs),
244 "Don't hide non-identity tabs when SMART is disabled. Useful for debugging.", NULL },
245 { "add-virtual", '\0', 0, G_OPTION_ARG_FILENAME_ARRAY, &(args.arg_add_virtual),
246 "Load smartctl data from file, creating a virtual drive. You can specify this option multiple times.", NULL },
247 { "add-device", '\0', 0, G_OPTION_ARG_FILENAME_ARRAY, &(args.arg_add_device),
248 "Add this device to device list. The format of the device is \"<device>::<type>::<extra_args>\", where type and extra_args are optional."
249 " This option is useful with --no-scan to list certain drives only. You can specify this option multiple times."
250 " Example: --add-device /dev/sda --add-device /dev/twa0::3ware,2 --add-device '/dev/sdb::::-T permissive'", NULL },
251 #ifndef _WIN32
252 // X11-specific
253 { "gdk-scale", 'l', 0, G_OPTION_ARG_DOUBLE, &(args.arg_gdk_scale),
254 "The value of GDK_SCALE environment variable (useful when executing with pkexec)", NULL },
255 { "gdk-dpi-scale", 'l', 0, G_OPTION_ARG_DOUBLE, &(args.arg_gdk_dpi_scale),
256 "The value of GDK_DPI_SCALE environment variable (useful when executing with pkexec)", NULL },
257 #endif
258 { NULL }
259 };
260
261 GError* error = 0;
262 GOptionContext* context = g_option_context_new("- A GTK+ GUI for smartmontools");
263
264 // our options
265 g_option_context_add_main_entries(context, arg_entries, NULL);
266
267 // gtk options
268 g_option_context_add_group(context, gtk_get_option_group(false));
269
270 // libdebug options; this will also automatically apply them
271 g_option_context_add_group(context, debug_get_option_group());
272
273 // The command-line parser stops at the first unknown option. Since this
274 // is kind of inconsistent, we abort altogether.
275 bool parsed = g_option_context_parse(context, &argc, &argv, &error);
276
277 if (error) {
278 std::string error_text = "\n" + std::string("Error parsing command-line options: ");
279 error_text += (error->message ? error->message : "invalid error");
280 error_text += "\n\n";
281 g_error_free(error);
282
283 #if (GLIB_CHECK_VERSION(2,14,0))
284 gchar* help_text = g_option_context_get_help(context, true, NULL);
285 if (help_text) {
286 error_text += help_text;
287 g_free(help_text);
288 }
289 #else
290 error_text += "Exiting.\n";
291 #endif
292
293 std::fprintf(stderr, "%s", error_text.c_str());
294 }
295 g_option_context_free(context);
296
297 return parsed;
298 }
299
300
301
302 /// Print application version information
app_print_version_info()303 inline void app_print_version_info()
304 {
305 std::string versiontext = std::string("\nGSmartControl version ") + VERSION + "\n";
306
307 std::string warningtext = std::string("\nWarning: GSmartControl");
308 warningtext += " comes with ABSOLUTELY NO WARRANTY.\n";
309 warningtext += "See LICENSE_gsmartcontrol.txt file for details.\n";
310 warningtext += "\nCopyright (C) 2008 - 2017 Alexander Shaduri <ashaduri" "" "@" "" "" "gmail.com>\n\n";
311
312 std::fprintf(stdout, "%s%s", versiontext.c_str(), warningtext.c_str());
313 }
314
315 }
316
317
318
319
app_init_and_loop(int & argc,char ** & argv)320 bool app_init_and_loop(int& argc, char**& argv)
321 {
322 // initialize GThread (for mutexes, etc... to work). Must be called before any other glib function.
323 // Glib::thread_init();
324
325 #ifdef _WIN32
326 // Disable client-side decorations (enable native windows decorations) under Windows.
327 hz::env_set_value("GTK_CSD", "0");
328 #endif
329
330 // Glib needs the C locale set to system locale for command line args.
331 // We will reset it later if needed.
332 hz::locale_c_set(""); // set the current locale to system locale
333
334 // Parse command line args.
335 // Due to gtk_get_option_group()/g_option_context_parse() calls, this
336 // will also initialize GTK and set the C locale to system locale (as well
337 // as do some locale-specific gdk initialization).
338 CmdArgs args;
339 if (! parse_cmdline_args(args, argc, argv)) {
340 return true;
341 }
342
343 // If locale setting is explicitly disabled, revert to the original classic C locale.
344 // Note that changing GTK locale after it's inited isn't really supported by GTK,
345 // but we have no other choice - glib needs system locale when parsing the
346 // arguments, and gtk is inited while the parsing is performed.
347 if (!args.arg_locale) {
348 hz::locale_c_set("C");
349 } else {
350 // change the C++ locale to match the C one.
351 hz::locale_cpp_set(""); // this may fail on some systems
352 }
353
354
355 if (args.arg_version) {
356 // show version information and exit
357 app_print_version_info();
358 return true;
359 }
360
361
362 // register libdebug domains
363 debug_register_domain("gtk");
364 debug_register_domain("app");
365 debug_register_domain("hz");
366 debug_register_domain("rmn");
367 debug_register_domain("rconfig");
368
369
370 // Add special debug channel to collect all libdebug output into a buffer.
371 debug_add_channel("all", debug_level::all, app_get_debug_buf_channel());
372
373
374
375 std::vector<std::string> load_virtuals;
376 if (args.arg_add_virtual) {
377 const gchar* entry = 0;
378 while ( (entry = *(args.arg_add_virtual)++) != NULL ) {
379 load_virtuals.push_back(entry);
380 }
381 }
382 std::string load_virtuals_str = hz::string_join(load_virtuals, ", "); // for display purposes only
383
384 std::vector<std::string> load_devices;
385 if (args.arg_add_device) {
386 const gchar* entry = 0;
387 while ( (entry = *(args.arg_add_device)++) != NULL ) {
388 load_devices.push_back(entry);
389 }
390 }
391 std::string load_devices_str = hz::string_join(load_devices, "; "); // for display purposes only
392
393
394 // it's here because earlier there are no domains
395 debug_out_dump("app", "Application options:\n"
396 << "\tlocale: " << args.arg_locale << "\n"
397 << "\tversion: " << args.arg_version << "\n"
398 << "\thide_tabs: " << args.arg_hide_tabs << "\n"
399 << "\tscan: " << args.arg_scan << "\n"
400 << "\targ_add_virtual: " << (load_virtuals_str.empty() ? "[empty]" : load_virtuals_str) << "\n"
401 << "\targ_add_device: " << (load_devices_str.empty() ? "[empty]" : load_devices_str) << "\n"
402 << "\targ_gdk_scale: " << args.arg_gdk_scale << "\n"
403 << "\targ_gdk_dpi_scale: " << args.arg_gdk_dpi_scale << "\n");
404
405 debug_out_dump("app", "LibDebug options:\n" << debug_get_cmd_args_dump());
406
407 #ifndef _WIN32
408 if (args.arg_gdk_scale == args.arg_gdk_scale) { // not NaN
409 hz::env_set_value("GDK_SCALE", hz::number_to_string(args.arg_gdk_scale));
410 }
411 if (args.arg_gdk_dpi_scale == args.arg_gdk_dpi_scale) { // not NaN
412 hz::env_set_value("GDK_DPI_SCALE", hz::number_to_string(args.arg_gdk_dpi_scale));
413 }
414 #endif
415
416
417 // Load config files
418 app_init_config();
419
420
421 // Redirect all GTK+/Glib and related messages to libdebug.
422 // Do this before GTK+ init, to capture its possible warnings as well.
423 static const char* const gtkdomains[] = {
424 // no atk or cairo, they don't log. libgnomevfs may be loaded by gtk file chooser.
425 "GLib", "GModule", "GLib-GObject", "GLib-GRegex", "GLib-GIO", "GThread",
426 "Pango", "Gtk", "Gdk", "GdkPixbuf", "libgnomevfs",
427 "glibmm", "giomm", "atkmm", "pangomm", "gdkmm", "gtkmm" };
428
429 for (unsigned int i = 0; i < G_N_ELEMENTS(gtkdomains); ++i) {
430 g_log_set_handler(gtkdomains[i], GLogLevelFlags(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL
431 | G_LOG_FLAG_RECURSION), glib_message_handler, NULL);
432 }
433
434
435 // Save the locale
436 std::locale final_loc_cpp = hz::locale_cpp_get<std::locale>();
437
438 // Initialize GTK+ (it's already initialized by command-line parser,
439 // so this doesn't do much).
440 // Newer gtkmm will try to set the C++ locale here.
441 // Note: passing false (as use_locale) as the third parameter here
442 // will generate a gtk_disable_setlocale() warning (due to gtk being
443 // already initialized), so manually save / restore the C++ locale
444 // (C locale won't be touched).
445 // Nothing is affected in gtkmm itself by C++ locale, so it's ok to do it.
446 Gtk::Main m(argc, argv);
447
448 // Restore the locale
449 hz::locale_cpp_set(final_loc_cpp);
450
451
452 debug_out_info("app", "Current C locale: " << hz::locale_c_get() << "\n");
453 debug_out_info("app", "Current C++ locale: " << hz::locale_cpp_get<std::string>() << "\n");
454
455
456 #ifdef _WIN32
457 // Now that all program-specific locale setup has been performed,
458 // make sure the future locale changes affect only current thread.
459 // Not available in mingw, so disable for now.
460 // _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
461 #endif
462
463
464 // This shows up in About dialog gtk.
465 Glib::set_application_name("GSmartControl"); // should be localized
466
467
468 // Add data file search paths
469 #ifdef _WIN32
470 // In windows the program is distributed with all the data files in the same directory.
471 hz::data_file_add_search_directory(".");
472 #else
473 #ifdef DEBUG_BUILD
474 hz::data_file_add_search_directory(std::string(TOP_SRC_DIR) + "/src/res"); // application data resources
475 hz::data_file_add_search_directory(std::string(TOP_SRC_DIR) + "/data"); // application data resources
476 #else
477 hz::data_file_add_search_directory(PACKAGE_PKGDATA_DIR); // /usr/share/program_name
478 #endif
479 #endif
480
481 #ifdef _WIN32
482 // Windows "Classic" theme is broken under GTK+3's "win32" theme.
483 // Make sure we fall back to Adwaita (which works, but looks non-native)
484 // for platforms which support "Classic" theme - Windows Server and Windows Vista / 7.
485 // Windows 8 / 10 don't support "Classic" so native look is preferred.
486 {
487 Glib::RefPtr<Gtk::Settings> gtk_settings = Gtk::Settings::get_default();
488 if (gtk_settings) {
489 Glib::ustring theme_name = gtk_settings->property_gtk_theme_name().get_value();
490 debug_out_dump("app", "Current GTK theme: " << theme_name << "\n");
491 if (IsWindowsServer() || !IsWindows8OrGreater()) {
492 if (theme_name == "win32") {
493 debug_out_dump("app", "Windows with Classic theme support detected, switching to Adwaita theme.\n");
494 gtk_settings->property_gtk_theme_name().set_value("Adwaita");
495 }
496 }
497 }
498 }
499 #endif
500
501 // Set default icon for all windows.
502 // Win32 version has its icon compiled-in, so no need to set it there.
503 #ifndef _WIN32
504 {
505 // we load it via icontheme to provide multi-size version.
506
507 // application-installed, /usr/share/icons/<theme_name>/apps/<size>
508 if (Gtk::IconTheme::get_default()->has_icon("gsmartcontrol")) {
509 Gtk::Window::set_default_icon_name("gsmartcontrol");
510
511 // try the gnome icon, it's higher quality / resolution
512 } else if (Gtk::IconTheme::get_default()->has_icon("gnome-dev-harddisk")) {
513 Gtk::Window::set_default_icon_name("gnome-dev-harddisk");
514
515 // gtk built-in, always available
516 } else {
517 Gtk::Window::set_default_icon_name("gtk-harddisk");
518 }
519 }
520 #endif
521
522
523 // Export some command line arguments to rmn
524
525 // obey the command line option for no-scan on startup
526 rconfig::set_data("/runtime/gui/force_no_scan_on_startup", !bool(args.arg_scan));
527
528 // load virtual drives on startup if specified.
529 rconfig::set_data("/runtime/gui/add_virtuals_on_startup", load_virtuals);
530
531 // add devices to the list on startup if specified.
532 rconfig::set_data("/runtime/gui/add_devices_on_startup", load_devices);
533
534 // hide tabs if SMART is disabled
535 rconfig::set_data("/runtime/gui/hide_tabs_on_smart_disabled", bool(args.arg_hide_tabs));
536
537
538 // Create executor log window, but don't show it.
539 // It will track all command executor outputs.
540 GscExecutorLogWindow::create();
541
542
543 // Open the main window
544 GscMainWindow* win = GscMainWindow::create();
545 if (!win) {
546 debug_out_fatal("app", "Cannot create the main window. Exiting.\n");
547 return false; // cannot create main window
548 }
549
550
551 // first-boot message
552 app_show_first_boot_message(win);
553
554
555 // The Main Loop (tm)
556 debug_out_info("app", "Entering main loop.\n");
557 m.run();
558 debug_out_info("app", "Main loop exited.\n");
559
560 // close the main window and delete its object
561 GscMainWindow::destroy();
562
563 GscExecutorLogWindow::destroy();
564
565
566 // std::cerr << app_get_debug_buffer_str(); // this will output everything that went through libdebug.
567
568
569 return true;
570 }
571
572
573
574
app_quit()575 void app_quit()
576 {
577 debug_out_info("app", "Saving config before exit...\n");
578
579 // save the config
580 #if defined ENABLE_GLIB && ENABLE_GLIB
581 rconfig::autosave_force_now();
582 #else
583 rconfig::save_to_file(s_home_config_file);
584 #endif
585
586 // exit the main loop
587 debug_out_info("app", "Trying to exit the main loop...\n");
588
589 Gtk::Main::quit();
590
591 // don't destroy main window here - we may be in one of its callbacks
592 }
593
594
595
596
597
598
599
600
601 /// @}
602