File Coverage

blib/lib/Gtk2/Ex/ErrorTextDialog/Handler.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             # Copyright 2009, 2010, 2011, 2013 Kevin Ryde
2              
3             # This file is part of Gtk2-Ex-ErrorTextDialog.
4             #
5             # Gtk2-Ex-ErrorTextDialog is free software; you can redistribute it and/or
6             # modify it under the terms of the GNU General Public License as published
7             # by the Free Software Foundation; either version 3, or (at your option) any
8             # later version.
9             #
10             # Gtk2-Ex-ErrorTextDialog is distributed in the hope that it will be useful,
11             # but WITHOUT ANY WARRANTY; without even the implied warranty of
12             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13             # Public License for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Gtk2-Ex-ErrorTextDialog. If not, see .
17              
18              
19             # If there's both errors and warnings from a "require" file then a
20             # $SIG{'__WARN__'} handler can run while PL_error_count is non-zero. In
21             # that case it's not possible to load modules within the warn handler, they
22             # get "BEGIN not safe after compilation error".
23             #
24             # The strategy against this is to pre-load enough to get a message to STDERR
25             # and install a Glib idle handler to load Gtk2::Ex::ErrorTextDialog and
26             # create that dialog.
27             #
28             package Gtk2::Ex::ErrorTextDialog::Handler;
29 1     1   932 use 5.008001; # for utf8::is_utf8() and PerlIO::get_layers()
  1         3  
  1         39  
30 1     1   6 use strict;
  1         2  
  1         35  
31 1     1   7 use warnings;
  1         3  
  1         35  
32 1     1   823 use Devel::GlobalDestruction ();
  1         2424  
  1         20  
33 1     1   636 use Glib;
  0            
  0            
34             use Encode;
35             use I18N::Langinfo; # CODESET
36             use PerlIO; # for F_UTF8
37              
38             our $VERSION = 11;
39              
40             # set this to 1 for some diagnostic prints (to STDERR)
41             use constant DEBUG => 0;
42              
43             my $_idle_another_message;
44             my $_idle_recursions = 0;
45             my $_idle_handler_id;
46              
47             our $exception_handler_depth = 0;
48              
49             sub exception_handler {
50             my ($msg) = @_;
51             if (DEBUG) { print STDERR "exception_handler() $exception_handler_depth\n"; }
52              
53             # Normally $SIG handlers run with themselves shadowed out, and the Glib
54             # exception handler doesn't re-invoke, so suspect warnings or errors in
55             # the code here won't recurse normally, but have this as some protection
56             # just in case.
57             #
58             if ($exception_handler_depth >= 3) {
59             return 1; # stay installed
60             }
61             if ($exception_handler_depth >= 2) {
62             print STDERR "ErrorTextDialog Handler: ignoring recursive exception_handler calls\n";
63             return 1; # stay installed
64             }
65             local $exception_handler_depth = $exception_handler_depth + 1;
66             if (DEBUG) { print STDERR " depth now $exception_handler_depth\n"; }
67              
68             #--------------------------------------------
69              
70             if (_fh_prints_wide('STDERR')) {
71             $msg = _maybe_locale_bytes_to_wide ($msg);
72             }
73             print STDERR $msg;
74              
75             #--------------------------------------------
76              
77             if ($_idle_recursions == 4) {
78             $_idle_recursions++;
79             print STDERR "ErrorTextDialog Handler: repeated messages adding to dialog, skip GUI from now on\n";
80              
81             } elsif ($_idle_recursions < 4
82             && ! Devel::GlobalDestruction::in_global_destruction()) {
83             $_idle_another_message = 1;
84             push @Gtk2::Ex::ErrorTextDialog::_instance_pending, $msg;
85              
86             # try to protect against unbounded growth of @_instance_pending
87             if (@Gtk2::Ex::ErrorTextDialog::_instance_pending > 500) {
88             splice @Gtk2::Ex::ErrorTextDialog::_instance_pending, 0, -500,
89             '[Big slew of pending messages truncated ...]';
90             }
91              
92             $_idle_handler_id ||= Glib::Idle->add
93             (\&_idle_handler, undef, Glib::G_PRIORITY_HIGH);
94             }
95              
96             if (DEBUG) { print STDERR "exception_handler() end\n"; }
97             return 1; # stay installed
98             }
99              
100             # $_idle_handler_id is zapped at the start so exception_handler() will add
101             # another _idle_handler() for any further messages generated within the
102             # present _idle_handler() run. Anything before popup_add_message() will be
103             # covered by the present run, but anything after it needs another run.
104             #
105             # $_idle_recursions is incremented at the start as a worst case assumption
106             # that the code will die. Then if the code runs successfully to the end it
107             # can be cleared. It's cleared only if there were no further messages
108             # generated from within _idle_handler(). Further messages are noted by
109             # exception_handler() setting $_idle_another_message.
110             #
111             sub _idle_handler {
112             $_idle_recursions++;
113             $_idle_another_message = 0;
114             undef $_idle_handler_id;
115             if (DEBUG) { print STDERR "idle_handler() runs $_idle_recursions\n"; }
116              
117             require Gtk2::Ex::ErrorTextDialog;
118             Gtk2::Ex::ErrorTextDialog->popup_add_message (undef);
119              
120             if (! $_idle_another_message) {
121             $_idle_recursions = 0;
122             }
123             if (DEBUG) {
124             print STDERR "idle_handler() end, recursions now $_idle_recursions\n";
125             }
126             return 0; # Glib::SOURCE_REMOVE
127             }
128              
129             sub log_handler {
130             require Gtk2::Ex::ErrorTextDialog;
131             exception_handler (Gtk2::Ex::ErrorTextDialog::_log_to_string (@_));
132             }
133              
134             #-----------------------------------------------------------------------------
135             # generic helpers
136              
137             # _fh_prints_wide($fh) returns true if wide chars can be printed to file
138             # handle $fh.
139             #
140             # PerlIO::get_layers() is pre-loaded, probably, but PerlIO::F_UTF8() from
141             # PerlIO.pm is not.
142             #
143             sub _fh_prints_wide {
144             my ($fh) = @_;
145             return (PerlIO::get_layers($fh, output => 1, details => 1))[-1] # top flags
146             & PerlIO::F_UTF8();
147             }
148              
149             # If $str is not wide, and it has some non-ascii, then try to decode them in
150             # the locale charset. PERLQQ means bad stuff is escaped.
151             sub _maybe_locale_bytes_to_wide {
152             my ($str) = @_;
153             if (! utf8::is_utf8 ($str) && $str =~ /[^[:ascii:]]/) {
154             require Encode;
155             $str = Encode::decode (_locale_charset_or_ascii(),
156             $str, Encode::FB_PERLQQ());
157             }
158             return $str;
159             }
160              
161             # _locale_charset_or_ascii() returns the locale charset from I18N::Langinfo,
162             # or 'ASCII' if nl_langinfo() is not available.
163             #
164             # langinfo() croaks "nl_langinfo() not implemented on this architecture" if
165             # not available. Though anywhere able to run Gtk would have nl_langinfo(),
166             # wouldn't it?
167             #
168             my $_locale_charset_or_ascii;
169             sub _locale_charset_or_ascii {
170             goto $_locale_charset_or_ascii;
171             }
172             BEGIN {
173             $_locale_charset_or_ascii = sub {
174             my $subr = sub { I18N::Langinfo::langinfo(I18N::Langinfo::CODESET()) };
175             if (! eval { &$subr(); 1 }) {
176             $subr = sub { 'ASCII' };
177             }
178             goto ($_locale_charset_or_ascii = $subr);
179             };
180             }
181              
182              
183             1;
184             __END__