1 |
dmorgan |
446 |
#!/usr/bin/perl |
2 |
|
|
|
3 |
|
|
use lib qw(/usr/lib/libDrakX); |
4 |
|
|
|
5 |
|
|
# prevent firing up drakbug (doesn't work w/o X11): |
6 |
|
|
BEGIN { $ENV{DISABLE_DRAKBUG} = 1 } |
7 |
|
|
|
8 |
|
|
use strict; |
9 |
|
|
use diagnostics; |
10 |
|
|
|
11 |
|
|
# fix translating reasons for 2nd stage warning |
12 |
|
|
use lang; |
13 |
|
|
BEGIN { |
14 |
|
|
my $locale = lang::read($>); |
15 |
|
|
lang::set($locale); |
16 |
|
|
} |
17 |
|
|
|
18 |
|
|
use standalone; #- warning, standalone must be loaded very first, for 'explanations' |
19 |
|
|
use c; |
20 |
|
|
use common; |
21 |
|
|
use interactive; |
22 |
|
|
use detect_devices; |
23 |
|
|
use harddrake::data; |
24 |
|
|
use harddrake::autoconf; |
25 |
|
|
use harddrake::sound; |
26 |
|
|
use Xconfig::card; |
27 |
|
|
use modules; |
28 |
|
|
use Storable qw(store retrieve); |
29 |
|
|
|
30 |
|
|
|
31 |
|
|
my $force = member('--force', @ARGV); |
32 |
|
|
|
33 |
|
|
my $invert_do_it = $ARGV[0] eq 'X11' ? 1 : 0; |
34 |
|
|
my ($hw_sysconfdir, $timeout) = ("/etc/sysconfig/harddrake2", $invert_do_it ? 600 : 25); |
35 |
|
|
my $last_boot_config = "$hw_sysconfdir/previous_hw"; |
36 |
|
|
|
37 |
|
|
$last_boot_config .= '_X11' if $invert_do_it; |
38 |
|
|
|
39 |
|
|
# first run ? if not read old hw config |
40 |
|
|
my $previous_config; |
41 |
|
|
if (-f $last_boot_config && -s $last_boot_config) { |
42 |
|
|
eval { $previous_config = Storable::retrieve($last_boot_config) }; |
43 |
|
|
log::explanations("resetting previous hardware file ($@)") if $@; |
44 |
|
|
} |
45 |
|
|
$previous_config ||= {}; |
46 |
|
|
$previous_config = $$previous_config if ref($previous_config) !~ /HASH/; |
47 |
|
|
my $first_run = is_empty_hash_ref($previous_config); |
48 |
|
|
|
49 |
|
|
my $modules_conf = modules::any_conf->read; |
50 |
|
|
|
51 |
|
|
my $isLaptop = detect_devices::isLaptop(); |
52 |
|
|
my $curr_kernel = c::kernel_version(); |
53 |
|
|
my ($kernel_major) = $curr_kernel =~ /^(\d+\.\d+)/; |
54 |
|
|
|
55 |
|
|
my %previous_kernel_config = getVarsFromSh("$hw_sysconfdir/kernel"); |
56 |
|
|
my %previous_xorg_config = getVarsFromSh("$hw_sysconfdir/xorg"); |
57 |
|
|
setVarsInSh("$hw_sysconfdir/kernel", { KERNEL => $kernel_major, IS_LAPTOP => bool2text($isLaptop) }); |
58 |
|
|
my %cfg = getVarsFromSh("$hw_sysconfdir/service.conf"); |
59 |
|
|
|
60 |
|
|
# default to 'yes' on upgrade: |
61 |
|
|
$cfg{AUTORECONFIGURE_RIGHT_XORG_DRIVER} = 'yes' if !exists $cfg{AUTORECONFIGURE_RIGHT_XORG_DRIVER}; |
62 |
|
|
|
63 |
|
|
# autoreconfigure laptop-dependent services when switching from laptop to desktop, and vice versa |
64 |
|
|
if (!exists $previous_kernel_config{IS_LAPTOP} || $force || $isLaptop != text2bool($previous_kernel_config{IS_LAPTOP})) { |
65 |
|
|
log::explanations("Autoconfiguring laptop tools since we switched between laptop and desktop systems"); |
66 |
|
|
harddrake::autoconf::laptop($isLaptop); |
67 |
|
|
} |
68 |
|
|
|
69 |
|
|
my $known_kernels = "$hw_sysconfdir/kernels"; |
70 |
|
|
if (!member($curr_kernel, chomp_(cat_($known_kernels)))) { |
71 |
|
|
harddrake::autoconf::fix_aliases($modules_conf) if !$first_run; |
72 |
|
|
append_to_file($known_kernels, "$curr_kernel\n"); |
73 |
|
|
} |
74 |
|
|
|
75 |
|
|
sub get_xorg_driver() { |
76 |
|
|
my $x = Xconfig::xfree->read; |
77 |
|
|
if ($x) { |
78 |
|
|
my ($dev_section) = grep { $_->{name} eq 'Device' } @{$x->{raw}}; |
79 |
|
|
$dev_section && $dev_section->{l}{Driver}{val}; |
80 |
|
|
} |
81 |
|
|
} |
82 |
|
|
|
83 |
|
|
sub schedule_warn_about_switch { |
84 |
|
|
my ($reason) = @_; |
85 |
|
|
output('/var/run/harddrake-notify-x11-free-driver-switch', $reason); |
86 |
|
|
} |
87 |
|
|
|
88 |
|
|
my $lib = arch() =~ /x86_64/ ? "lib64" : "lib"; |
89 |
|
|
|
90 |
|
|
sub find_xorg_driver { |
91 |
|
|
my ($new_driver) = @_; |
92 |
|
|
# nvidia driver has special place: |
93 |
|
|
-e "/usr/$lib/xorg/modules/drivers/${new_driver}_drv.so" |
94 |
|
|
|| -e "/usr/$lib/xorg/extra-modules/${new_driver}_drv.so"; |
95 |
|
|
} |
96 |
|
|
|
97 |
|
|
sub switch_x_driver { |
98 |
|
|
my ($old_driver, $new_driver, $reason) = @_; |
99 |
|
|
if (!find_xorg_driver($new_driver)) { |
100 |
|
|
log::explanations("would switch X.org driver from '$old_driver' to '$new_driver' ($reason); but new driver is not installed"); |
101 |
|
|
return; |
102 |
|
|
} |
103 |
|
|
cp_af('/etc/X11/xorg.conf', "/etc/X11/xorg.conf.mga$^T"); |
104 |
|
|
substInFile { s!Driver "($old_driver)"!Driver "$new_driver"!g } '/etc/X11/xorg.conf'; |
105 |
|
|
log::explanations("switch X.org driver from '$old_driver' to '$new_driver' ($reason)"); |
106 |
|
|
Xconfig::card::libgl_config_and_more({ Driver => $new_driver }); |
107 |
|
|
} |
108 |
|
|
|
109 |
|
|
sub should_reconfigure_x_driver { |
110 |
|
|
my ($card_data, $device, $current_driver) = @_; |
111 |
|
|
my $reason; |
112 |
|
|
my $reconfigure; |
113 |
|
|
my $new_key = $card_data->{Driver} . $card_data->{Driver2}; |
114 |
|
|
setVarsInSh("$hw_sysconfdir/xorg", { XORG_DRV => $new_key }); |
115 |
|
|
# auto reconfigure x11 only on first time default driver have changed: |
116 |
|
|
if ($previous_xorg_config{XORG_DRV} ne $new_key) { |
117 |
|
|
if (!member($current_driver, $card_data->{Driver}, $card_data->{Driver2}, 'fbdev', 'vesa')) { |
118 |
|
|
$reason = N("The graphic card '%s' is no more supported by the '%s' driver", |
119 |
|
|
$device->{description}, $current_driver); |
120 |
|
|
$reconfigure = 1; |
121 |
|
|
} |
122 |
|
|
} elsif ((stat('/etc/X11/xorg.conf'))[9] < (stat('/etc/product.id'))[9]) { |
123 |
|
|
# when switching to a new release (product.id is newer than xorg.conf), |
124 |
|
|
# regenerate xorg.conf even if the driver used is vesa or fbdev, this |
125 |
|
|
# way we handle switches like "no driver for the card in older releases |
126 |
|
|
# but good driver in new release", see bug #53753 |
127 |
|
|
if (!member($current_driver, $card_data->{Driver}, $card_data->{Driver2})) { |
128 |
|
|
$reason = N("New release, reconfiguring X for %s", $device->{description}); |
129 |
|
|
$reconfigure = 1; |
130 |
|
|
} |
131 |
|
|
} |
132 |
|
|
|
133 |
|
|
($reconfigure, $reason); |
134 |
|
|
} |
135 |
|
|
|
136 |
|
|
my @cards = ( |
137 |
|
|
{ |
138 |
|
|
ldetect_driver_regexp => 'Card:NVIDIA', |
139 |
|
|
xorg_driver_regexp => 'nv.+', |
140 |
|
|
module_names => [ qw(NVdriver nvidia.o nvidia.ko nvidia71xx.ko nvidia96xx.ko nvidia97xx.ko nvidia173.ko nvidia-current.ko) ] |
141 |
|
|
}, |
142 |
|
|
{ |
143 |
|
|
ldetect_driver_regexp => 'Card:ATI Radeon', |
144 |
|
|
xorg_driver_regexp => 'fglrx', |
145 |
|
|
module_names => [ qw(fglrx.ko fglrx-hd2000.ko) ] |
146 |
|
|
} |
147 |
|
|
); |
148 |
|
|
|
149 |
|
|
|
150 |
|
|
my @devices; |
151 |
|
|
@devices = grep { $_->{driver} =~ /^Card:/ } detect_devices::probeall() |
152 |
|
|
if -f '/etc/X11/xorg.conf'; |
153 |
|
|
|
154 |
|
|
# do not auto reconfigure if more than one graphic card: |
155 |
|
|
$cfg{AUTORECONFIGURE_RIGHT_XORG_DRIVER} = 'no' if scalar(@devices) > 1; |
156 |
|
|
|
157 |
|
|
foreach my $device (@devices) { |
158 |
|
|
next if !text2bool($cfg{AUTORECONFIGURE_RIGHT_XORG_DRIVER}); |
159 |
|
|
|
160 |
|
|
my $id = $device->{driver} =~ /Card:(.*)/ && $1; |
161 |
|
|
my $card_data = Xconfig::card::readCardsDB("/usr/share/ldetect-lst/Cards+")->{$id}; |
162 |
|
|
my $current_driver = get_xorg_driver(); |
163 |
|
|
|
164 |
|
|
# nvidia proprietary driver in ldetect-lst can be 'nvidia173', 'nvidia-current', ... |
165 |
|
|
# but really is just 'nvidia' in xorg.conf: |
166 |
|
|
$card_data->{Driver2} =~ s/(nvidia).*/$1/; |
167 |
|
|
|
168 |
|
|
# auto reconfigure x11 only on first time default driver have changed: |
169 |
|
|
my ($should_reconfigure, $reason) = should_reconfigure_x_driver($card_data, $device, $current_driver); |
170 |
|
|
if ($should_reconfigure) { |
171 |
|
|
switch_x_driver($current_driver, $card_data->{Driver}, $reason); |
172 |
|
|
schedule_warn_about_switch($reason) if any { $current_driver =~ $_->{xorg_driver_regexp} } @cards; |
173 |
|
|
# Update $current_driver with the new one |
174 |
|
|
$current_driver = $card_data->{Driver}; |
175 |
|
|
} |
176 |
|
|
|
177 |
|
|
# nv->nouveau or non_kms_nouveau->kms_nouveau can't have "Disable dri"! |
178 |
|
|
if ($current_driver eq "nouveau") { |
179 |
|
|
my $raw_x = Xconfig::xfree->read; |
180 |
|
|
if ($raw_x) { |
181 |
|
|
if (member("dri", $raw_x->get_disabled_modules)) { |
182 |
|
|
$raw_x->remove_disable_module("dri"); |
183 |
|
|
$raw_x->write; |
184 |
|
|
} |
185 |
|
|
} |
186 |
|
|
} |
187 |
|
|
} |
188 |
|
|
|
189 |
|
|
|
190 |
|
|
foreach my $card (@cards) { |
191 |
|
|
my $device = find { $_->{driver} =~ /$card->{ldetect_driver_regexp}/ } @devices; |
192 |
|
|
next if !$device; |
193 |
|
|
|
194 |
|
|
if (find { -e join('', "/lib/modules/", c::kernel_version(), $_) } |
195 |
|
|
map { ("/dkms/$_", "/dkms-binary/$_", "/kernel/$_") } map { "/drivers/$_" } map { ("extra/$_", "video/$_", "char/$_", "char/drm/$_") } map { $_, "$_.gz" } @{$card->{module_names}}) { |
196 |
|
|
|
197 |
|
|
# do not automatically switch from nv to nvidia (in order to handle |
198 |
|
|
# cases where nvidia module crashes the system): |
199 |
|
|
# |
200 |
|
|
# substInFile { |
201 |
|
|
# log::explanations("switch XFree86 driver from nv to nvidia") if /Driver "nv"/; |
202 |
|
|
# s!Driver "nv.*"!Driver "nvidia"!g; |
203 |
|
|
# s!#*( Load.*glx)!\1!g; |
204 |
|
|
# } $_ foreach "/etc/X11/XF86Config-4", "/etc/X11/XF86Config"; |
205 |
|
|
} else { |
206 |
|
|
my @cards = Xconfig::card::probe(); |
207 |
|
|
my $driver = $cards[0]{Driver}; |
208 |
|
|
my $old_driver = cat_('/etc/X11/xorg.conf') =~ /Driver "($card->{xorg_driver_regexp})"/ && $1; |
209 |
|
|
if ($old_driver) { |
210 |
|
|
my $reason = N("The proprietary kernel driver was not found for '%s' X.org driver", |
211 |
|
|
$old_driver); |
212 |
|
|
switch_x_driver($card->{xorg_driver_regexp}, $driver, $reason); |
213 |
|
|
schedule_warn_about_switch($reason); |
214 |
|
|
} |
215 |
|
|
} |
216 |
|
|
} |
217 |
|
|
|
218 |
|
|
my $is_globetrotter = -f '/usr/sbin/mdkmove'; |
219 |
|
|
|
220 |
|
|
my (%config, $wait); |
221 |
|
|
my $in; |
222 |
|
|
my $splash = -f '/proc/splash'; |
223 |
|
|
my $splash_was_silent = cat_('/proc/splash') =~ /, silent : on/; |
224 |
|
|
|
225 |
|
|
# For each hw, class, detect device, compare and offer to reconfigure if needed |
226 |
|
|
foreach my $hw_class (@harddrake::data::tree) { |
227 |
|
|
my ($Ident, $item, $configurator, $detector, $do_it) = @$hw_class{qw(class string configurator detector checked_on_boot)}; |
228 |
|
|
next if member($cfg{"DETECT_$Ident"}, qw(NO no)); |
229 |
|
|
|
230 |
|
|
$configurator ||= $hw_class->{configurator}; |
231 |
|
|
|
232 |
|
|
next unless $do_it ^ $invert_do_it; |
233 |
|
|
# No detector ? (should never happen but who know ?) |
234 |
|
|
ref($detector) eq 'CODE' or next; |
235 |
|
|
|
236 |
|
|
my %ID = map { |
237 |
|
|
my $i = $_; |
238 |
|
|
my $id = defined $i->{device} ? $i->{device} : join(':', map { $i->{$_} } qw(vendor id subvendor subid)); |
239 |
|
|
$id => $i; |
240 |
|
|
} eval { $detector->({}) }; |
241 |
|
|
$config{$Ident} = \%ID; |
242 |
|
|
next if !$is_globetrotter && !$force && $first_run; # do not fsck on first run but if --force |
243 |
|
|
|
244 |
|
|
my $oldconfig = $force ? {} : $previous_config->{$Ident}; |
245 |
|
|
|
246 |
|
|
my $msg; |
247 |
|
|
my @was_removed = difference2([ keys %$oldconfig ], [ keys %ID ]); |
248 |
|
|
if (@was_removed) { |
249 |
|
|
$msg .= N("Some devices in the \"%s\" hardware class were removed:\n", $item) . |
250 |
|
|
join('', map { N("- %s was removed\n", harddrake::data::custom_id($oldconfig->{$_}, $item)) } @was_removed) . "\n"; |
251 |
|
|
} |
252 |
|
|
my @added = difference2([ keys %ID ], [ keys %$oldconfig ]); |
253 |
|
|
$msg .= N("Some devices were added: %s\n", $item) if @added; |
254 |
|
|
$msg .= N("- %s was added\n", harddrake::data::custom_id($ID{$_}, $item)) foreach @added; |
255 |
|
|
log::explanations("removed $Ident: " . harddrake::data::custom_id($oldconfig->{$_}, $item)) foreach @was_removed; |
256 |
|
|
log::explanations("added $Ident: " . harddrake::data::custom_id($ID{$_}, $item)) foreach @added; |
257 |
|
|
|
258 |
|
|
if ($Ident eq 'FIREWIRE_CONTROLLER' && any { $_->{driver} eq 'ohci1394' } @ID{@added}) { |
259 |
|
|
modules::load_and_configure($modules_conf, 'ohci1394'); |
260 |
|
|
$modules_conf->write; |
261 |
|
|
} |
262 |
|
|
|
263 |
|
|
@added || @was_removed or $cfg{"DETECT_$Ident"} ne 'force' and next; |
264 |
|
|
|
265 |
|
|
next if $Ident eq 'MOUSE' && $kernel_major ne $previous_kernel_config{KERNEL} && $cfg{"DETECT_$Ident"} ne 'force'; |
266 |
|
|
|
267 |
|
|
my @configurator_pool = $configurator; |
268 |
|
|
|
269 |
|
|
if ($Ident eq "AUDIO") { |
270 |
|
|
# automatic sound slots configuration |
271 |
|
|
rm_rf("/etc/asound.state") if -e "/etc/asound.state"; |
272 |
|
|
harddrake::sound::configure_sound_slots($modules_conf); |
273 |
|
|
next; |
274 |
|
|
} elsif ($Ident eq "ETHERNET") { |
275 |
|
|
$modules_conf->remove_alias_regexp('^(wlan|eth)[0-9]*$'); |
276 |
|
|
modules::load_category($modules_conf, 'network/main|gigabit|usb|wireless|firewire|pcmcia'); |
277 |
|
|
require network::connection::ethernet; |
278 |
|
|
network::connection::ethernet::configure_eth_aliases($modules_conf); |
279 |
|
|
require network::rfswitch; |
280 |
|
|
network::rfswitch::configure(); |
281 |
|
|
require network::shorewall; |
282 |
|
|
network::shorewall::update_interfaces_list(); |
283 |
|
|
$modules_conf->write; |
284 |
|
|
next; |
285 |
|
|
} elsif (member($Ident, qw(ATA_STORAGE CARD_READER RAID_STORAGE SATA_STORAGE SCSI_CONTROLLER))) { |
286 |
|
|
# set scsi_hostadapter in modprobe.conf: |
287 |
|
|
modules::load_category($modules_conf, 'disk/' . { |
288 |
|
|
ATA_STORAGE => 'ide', |
289 |
|
|
SATA_STORAGE => 'sata', |
290 |
|
|
SCSI_CONTROLLER => 'scsi', |
291 |
|
|
RAID_STORAGE => 'hardware_raid', |
292 |
|
|
CARD_READER => 'card_reader' |
293 |
|
|
}->{$Ident}); |
294 |
|
|
$modules_conf->write; |
295 |
|
|
next; |
296 |
|
|
} elsif (member($Ident, qw(AGP DVB TV))) { |
297 |
|
|
my @old_drivers = uniq(map { $_->{driver} } values %$oldconfig); |
298 |
|
|
my @new_drivers = uniq(map { $_->{driver} } values %ID); |
299 |
|
|
# load DVB & TV drivers (eg: for One), not for AGP (done by X): |
300 |
|
|
modules::load_category($modules_conf, 'multimedia/' . lc($Ident)) if member($Ident, qw(DVB TV)); |
301 |
|
|
$modules_conf->remove_module(difference2(\@old_drivers, \@new_drivers)); |
302 |
|
|
# add agpgart and the like modules to modprobe.preload if needed: |
303 |
|
|
$modules_conf->write; |
304 |
|
|
foreach (difference2(\@new_drivers, \@old_drivers)) { |
305 |
|
|
eval { modules::load($_) }; |
306 |
|
|
warn "warning: $@" if $@; |
307 |
|
|
} |
308 |
|
|
next; |
309 |
|
|
} elsif ($Ident eq "BLUETOOTH") { |
310 |
|
|
harddrake::autoconf::bluetooth(scalar keys %ID); |
311 |
|
|
} elsif ($Ident eq "PCMCIA_CONTROLLER") { |
312 |
|
|
harddrake::autoconf::pcmcia(keys %ID ? first(values(%ID))->{driver} : ''); |
313 |
|
|
} elsif ($Ident eq "USB_CONTROLLER") { |
314 |
|
|
# nearly useless (only mkinitrd uses it): |
315 |
|
|
modules::load_category($modules_conf, 'bus/usb'); |
316 |
|
|
$modules_conf->write; |
317 |
|
|
} elsif ($Ident eq "VIDEO") { |
318 |
|
|
# explicitely NOT read the existing config (eg: new profile with globetrotter) |
319 |
|
|
harddrake::autoconf::xconf($modules_conf, {}, member($cfg{SETUP_FB}, qw(NO no)), $cfg{RESOLUTION_WANTED}); |
320 |
|
|
next; |
321 |
|
|
} elsif ($Ident eq "MOUSE") { |
322 |
|
|
harddrake::autoconf::mouse_conf($modules_conf); |
323 |
|
|
next; |
324 |
|
|
} elsif ($Ident eq "CPU") { |
325 |
|
|
harddrake::autoconf::cpufreq(); |
326 |
|
|
} elsif ($Ident eq "FLOPPY") { |
327 |
|
|
harddrake::autoconf::floppy(); |
328 |
|
|
} |
329 |
|
|
|
330 |
|
|
next if $is_globetrotter && !$hw_class->{automatic}; |
331 |
|
|
next unless $configurator_pool[0]; |
332 |
|
|
if (ref($configurator) ne 'CODE' && !-x first(split /\s+/, $configurator_pool[0])) { |
333 |
|
|
log::explanations(qw(skip $Ident configuration since "$configurator" is not executable)); |
334 |
|
|
next; |
335 |
|
|
} |
336 |
|
|
my ($pid, $no, $res); |
337 |
|
|
$hw_class->{automatic} ||= ref($configurator) eq 'CODE'; |
338 |
|
|
|
339 |
|
|
if (!$hw_class->{automatic}) { |
340 |
|
|
$SIG{ALRM} = sub { $no = 1; kill 15, $pid }; |
341 |
|
|
unless ($pid = fork()) { |
342 |
|
|
$splash and eval { output('/proc/splash', 'verbose') } and $splash = 0; |
343 |
|
|
exec("/usr/share/harddrake/confirm", $Ident, $timeout, $msg); |
344 |
|
|
} |
345 |
|
|
alarm($timeout); |
346 |
|
|
wait(); |
347 |
|
|
$res = $?; |
348 |
|
|
alarm(0); |
349 |
|
|
} else { |
350 |
|
|
$res = 1; |
351 |
|
|
} |
352 |
|
|
if (ref($configurator) eq 'CODE') { |
353 |
|
|
eval { $configurator->() }; |
354 |
|
|
log::explanations(qw(cannot run "$configurator": $@)) if $@; |
355 |
|
|
} elsif (!$no && $res) { |
356 |
|
|
foreach my $program (@configurator_pool) { |
357 |
|
|
if (fork()) { |
358 |
|
|
wait(); |
359 |
|
|
} else { |
360 |
|
|
log::explanations(qq(run "$program")); |
361 |
|
|
exec("$program 2>/dev/null") or do { |
362 |
|
|
log::explanations(qq(cannot run "$program")); |
363 |
|
|
require POSIX; |
364 |
|
|
POSIX::_exit(); |
365 |
|
|
}; |
366 |
|
|
} |
367 |
|
|
} |
368 |
|
|
} |
369 |
|
|
if (!$hw_class->{automatic}) { |
370 |
|
|
require interactive; |
371 |
|
|
undef $wait; |
372 |
|
|
$in ||= interactive->vnew; |
373 |
|
|
$wait = $in->wait_message(N("Please wait"), N("Hardware probing in progress")); |
374 |
|
|
} |
375 |
|
|
|
376 |
|
|
} |
377 |
|
|
|
378 |
|
|
# output new hw config |
379 |
|
|
log::explanations("created file $last_boot_config"); |
380 |
|
|
Storable::store(\%config, $last_boot_config); |
381 |
|
|
|
382 |
|
|
# restore bootsplash mode |
383 |
|
|
$splash_was_silent and eval { output('/proc/splash', 'silent') }; |
384 |
|
|
|
385 |
|
|
|
386 |
|
|
$in->exit(0) if $in; |