1package Test2::IPC::Driver::Files; 2use strict; 3use warnings; 4 5our $VERSION = '1.302133'; 6 7 8BEGIN { require Test2::IPC::Driver; our @ISA = qw(Test2::IPC::Driver) } 9 10use Test2::Util::HashBase qw{tempdir event_ids read_ids timeouts tid pid globals}; 11 12use Scalar::Util qw/blessed/; 13use File::Temp(); 14use Storable(); 15use File::Spec(); 16use POSIX(); 17 18use Test2::Util qw/try get_tid pkg_to_file IS_WIN32 ipc_separator do_rename do_unlink try_sig_mask/; 19use Test2::API qw/test2_ipc_set_pending/; 20 21sub use_shm { 1 } 22sub shm_size() { 64 } 23 24sub is_viable { 1 } 25 26sub init { 27 my $self = shift; 28 29 my $tmpdir = File::Temp::tempdir( 30 $ENV{T2_TEMPDIR_TEMPLATE} || "test2" . ipc_separator . $$ . ipc_separator . "XXXXXX", 31 CLEANUP => 0, 32 TMPDIR => 1, 33 ); 34 35 $self->abort_trace("Could not get a temp dir") unless $tmpdir; 36 37 $self->{+TEMPDIR} = File::Spec->canonpath($tmpdir); 38 39 print STDERR "\nIPC Temp Dir: $tmpdir\n\n" 40 if $ENV{T2_KEEP_TEMPDIR}; 41 42 $self->{+EVENT_IDS} = {}; 43 $self->{+READ_IDS} = {}; 44 $self->{+TIMEOUTS} = {}; 45 46 $self->{+TID} = get_tid(); 47 $self->{+PID} = $$; 48 49 $self->{+GLOBALS} = {}; 50 51 return $self; 52} 53 54sub hub_file { 55 my $self = shift; 56 my ($hid) = @_; 57 my $tdir = $self->{+TEMPDIR}; 58 return File::Spec->catfile($tdir, "HUB" . ipc_separator . $hid); 59} 60 61sub event_file { 62 my $self = shift; 63 my ($hid, $e) = @_; 64 65 my $tempdir = $self->{+TEMPDIR}; 66 my $type = blessed($e) or $self->abort("'$e' is not a blessed object!"); 67 68 $self->abort("'$e' is not an event object!") 69 unless $type->isa('Test2::Event'); 70 71 my $tid = get_tid(); 72 my $eid = $self->{+EVENT_IDS}->{$hid}->{$$}->{$tid} += 1; 73 74 my @type = split '::', $type; 75 my $name = join(ipc_separator, $hid, $$, $tid, $eid, @type); 76 77 return File::Spec->catfile($tempdir, $name); 78} 79 80sub add_hub { 81 my $self = shift; 82 my ($hid) = @_; 83 84 my $hfile = $self->hub_file($hid); 85 86 $self->abort_trace("File for hub '$hid' already exists") 87 if -e $hfile; 88 89 open(my $fh, '>', $hfile) or $self->abort_trace("Could not create hub file '$hid': $!"); 90 print $fh "$$\n" . get_tid() . "\n"; 91 close($fh); 92} 93 94sub drop_hub { 95 my $self = shift; 96 my ($hid) = @_; 97 98 my $tdir = $self->{+TEMPDIR}; 99 my $hfile = $self->hub_file($hid); 100 101 $self->abort_trace("File for hub '$hid' does not exist") 102 unless -e $hfile; 103 104 open(my $fh, '<', $hfile) or $self->abort_trace("Could not open hub file '$hid': $!"); 105 my ($pid, $tid) = <$fh>; 106 close($fh); 107 108 $self->abort_trace("A hub file can only be closed by the process that started it\nExpected $pid, got $$") 109 unless $pid == $$; 110 111 $self->abort_trace("A hub file can only be closed by the thread that started it\nExpected $tid, got " . get_tid()) 112 unless get_tid() == $tid; 113 114 if ($ENV{T2_KEEP_TEMPDIR}) { 115 my ($ok, $err) = do_rename($hfile, File::Spec->canonpath("$hfile.complete")); 116 $self->abort_trace("Could not rename file '$hfile' -> '$hfile.complete': $err") unless $ok 117 } 118 else { 119 my ($ok, $err) = do_unlink($hfile); 120 $self->abort_trace("Could not remove file for hub '$hid': $err") unless $ok 121 } 122 123 opendir(my $dh, $tdir) or $self->abort_trace("Could not open temp dir!"); 124 for my $file (readdir($dh)) { 125 next if $file =~ m{\.complete$}; 126 next unless $file =~ m{^$hid}; 127 $self->abort_trace("Not all files from hub '$hid' have been collected!"); 128 } 129 closedir($dh); 130} 131 132sub send { 133 my $self = shift; 134 my ($hid, $e, $global) = @_; 135 136 my $tempdir = $self->{+TEMPDIR}; 137 my $hfile = $self->hub_file($hid); 138 my $dest = $global ? 'GLOBAL' : $hid; 139 140 $self->abort(<<" EOT") unless $global || -f $hfile; 141hub '$hid' is not available, failed to send event! 142 143There was an attempt to send an event to a hub in a parent process or thread, 144but that hub appears to be gone. This can happen if you fork, or start a new 145thread from inside subtest, and the parent finishes the subtest before the 146child returns. 147 148This can also happen if the parent process is done testing before the child 149finishes. Test2 normally waits automatically in the root process, but will not 150do so if Test::Builder is loaded for legacy reasons. 151 EOT 152 153 my $file = $self->event_file($dest, $e); 154 my $ready = File::Spec->canonpath("$file.ready"); 155 156 if ($global) { 157 my $name = $ready; 158 $name =~ s{^.*(GLOBAL)}{GLOBAL}; 159 $self->{+GLOBALS}->{$hid}->{$name}++; 160 } 161 162 # Write and rename the file. 163 my ($ren_ok, $ren_err); 164 my ($ok, $err) = try_sig_mask { 165 Storable::store($e, $file); 166 ($ren_ok, $ren_err) = do_rename("$file", $ready); 167 }; 168 169 if ($ok) { 170 $self->abort("Could not rename file '$file' -> '$ready': $ren_err") unless $ren_ok; 171 test2_ipc_set_pending(substr($file, -(shm_size))); 172 } 173 else { 174 my $src_file = __FILE__; 175 $err =~ s{ at \Q$src_file\E.*$}{}; 176 chomp($err); 177 my $tid = get_tid(); 178 my $trace = $e->trace->debug; 179 my $type = blessed($e); 180 181 $self->abort(<<" EOT"); 182 183******************************************************************************* 184There was an error writing an event: 185Destination: $dest 186Origin PID: $$ 187Origin TID: $tid 188Event Type: $type 189Event Trace: $trace 190File Name: $file 191Ready Name: $ready 192Error: $err 193******************************************************************************* 194 195 EOT 196 } 197 198 return 1; 199} 200 201sub driver_abort { 202 my $self = shift; 203 my ($msg) = @_; 204 205 local ($@, $!, $?, $^E); 206 eval { 207 my $abort = File::Spec->catfile($self->{+TEMPDIR}, "ABORT"); 208 open(my $fh, '>>', $abort) or die "Could not open abort file: $!"; 209 print $fh $msg, "\n"; 210 close($fh) or die "Could not close abort file: $!"; 211 1; 212 } or warn $@; 213} 214 215sub cull { 216 my $self = shift; 217 my ($hid) = @_; 218 219 my $tempdir = $self->{+TEMPDIR}; 220 221 opendir(my $dh, $tempdir) or $self->abort("could not open IPC temp dir ($tempdir)!"); 222 223 my $read = $self->{+READ_IDS}; 224 my $timeouts = $self->{+TIMEOUTS}; 225 226 my @out; 227 for my $info (sort cmp_events map { $self->should_read_event($hid, $_) } readdir($dh)) { 228 unless ($info->{global}) { 229 my $next = $self->{+READ_IDS}->{$info->{hid}}->{$info->{pid}}->{$info->{tid}} ||= 1; 230 231 $timeouts->{$info->{file}} ||= time; 232 233 if ($next != $info->{eid}) { 234 # Wait up to N seconds for missing events 235 next unless 5 < time - $timeouts->{$info->{file}}; 236 $self->abort("Missing event HID: $info->{hid}, PID: $info->{pid}, TID: $info->{tid}, EID: $info->{eid}."); 237 } 238 239 $self->{+READ_IDS}->{$info->{hid}}->{$info->{pid}}->{$info->{tid}} = $info->{eid} + 1; 240 } 241 242 my $full = $info->{full_path}; 243 my $obj = $self->read_event_file($full); 244 push @out => $obj; 245 246 # Do not remove global events 247 next if $info->{global}; 248 249 if ($ENV{T2_KEEP_TEMPDIR}) { 250 my $complete = File::Spec->canonpath("$full.complete"); 251 my ($ok, $err) = do_rename($full, $complete); 252 $self->abort("Could not rename IPC file '$full', '$complete': $err") unless $ok; 253 } 254 else { 255 my ($ok, $err) = do_unlink("$full"); 256 $self->abort("Could not unlink IPC file '$full': $err") unless $ok; 257 } 258 } 259 260 closedir($dh); 261 return @out; 262} 263 264sub parse_event_filename { 265 my $self = shift; 266 my ($file) = @_; 267 268 # The || is to force 0 in false 269 my $complete = substr($file, -9, 9) eq '.complete' || 0 and substr($file, -9, 9, ""); 270 my $ready = substr($file, -6, 6) eq '.ready' || 0 and substr($file, -6, 6, ""); 271 272 my @parts = split ipc_separator, $file; 273 my ($global, $hid) = $parts[0] eq 'GLOBAL' ? (1, shift @parts) : (0, join ipc_separator, splice(@parts, 0, 3)); 274 my ($pid, $tid, $eid) = splice(@parts, 0, 3); 275 my $type = join '::' => @parts; 276 277 return { 278 file => $file, 279 ready => $ready, 280 complete => $complete, 281 global => $global, 282 type => $type, 283 hid => $hid, 284 pid => $pid, 285 tid => $tid, 286 eid => $eid, 287 }; 288} 289 290sub should_read_event { 291 my $self = shift; 292 my ($hid, $file) = @_; 293 294 return if substr($file, 0, 1) eq '.'; 295 return if substr($file, 0, 3) eq 'HUB'; 296 CORE::exit(255) if $file eq 'ABORT'; 297 298 my $parsed = $self->parse_event_filename($file); 299 300 return if $parsed->{complete}; 301 return unless $parsed->{ready}; 302 return unless $parsed->{global} || $parsed->{hid} eq $hid; 303 304 return if $parsed->{global} && $self->{+GLOBALS}->{$hid}->{$file}++; 305 306 # Untaint the path. 307 my $full = File::Spec->catfile($self->{+TEMPDIR}, $file); 308 ($full) = ($full =~ m/^(.*)$/gs) if ${^TAINT}; 309 310 $parsed->{full_path} = $full; 311 312 return $parsed; 313} 314 315sub cmp_events { 316 # Globals first 317 return -1 if $a->{global} && !$b->{global}; 318 return 1 if $b->{global} && !$a->{global}; 319 320 return $a->{pid} <=> $b->{pid} 321 || $a->{tid} <=> $b->{tid} 322 || $a->{eid} <=> $b->{eid}; 323} 324 325sub read_event_file { 326 my $self = shift; 327 my ($file) = @_; 328 329 my $obj = Storable::retrieve($file); 330 $self->abort("Got an unblessed object: '$obj'") 331 unless blessed($obj); 332 333 unless ($obj->isa('Test2::Event')) { 334 my $pkg = blessed($obj); 335 my $mod_file = pkg_to_file($pkg); 336 my ($ok, $err) = try { require $mod_file }; 337 338 $self->abort("Event has unknown type ($pkg), tried to load '$mod_file' but failed: $err") 339 unless $ok; 340 341 $self->abort("'$obj' is not a 'Test2::Event' object") 342 unless $obj->isa('Test2::Event'); 343 } 344 345 return $obj; 346} 347 348sub waiting { 349 my $self = shift; 350 require Test2::Event::Waiting; 351 $self->send( 352 GLOBAL => Test2::Event::Waiting->new( 353 trace => Test2::EventFacet::Trace->new(frame => [caller()]), 354 ), 355 'GLOBAL' 356 ); 357 return; 358} 359 360sub DESTROY { 361 my $self = shift; 362 363 return unless defined $self->pid; 364 return unless defined $self->tid; 365 366 return unless $$ == $self->pid; 367 return unless get_tid() == $self->tid; 368 369 my $tempdir = $self->{+TEMPDIR}; 370 371 my $aborted = 0; 372 my $abort_file = File::Spec->catfile($self->{+TEMPDIR}, "ABORT"); 373 if (-e $abort_file) { 374 $aborted = 1; 375 my ($ok, $err) = do_unlink($abort_file); 376 warn $err unless $ok; 377 } 378 379 opendir(my $dh, $tempdir) or $self->abort("Could not open temp dir! ($tempdir)"); 380 while(my $file = readdir($dh)) { 381 next if $file =~ m/^\.+$/; 382 next if $file =~ m/\.complete$/; 383 my $full = File::Spec->catfile($tempdir, $file); 384 385 my $sep = ipc_separator; 386 if ($aborted || $file =~ m/^(GLOBAL|HUB$sep)/) { 387 $full =~ m/^(.*)$/; 388 $full = $1; # Untaint it 389 next if $ENV{T2_KEEP_TEMPDIR}; 390 my ($ok, $err) = do_unlink($full); 391 $self->abort("Could not unlink IPC file '$full': $err") unless $ok; 392 next; 393 } 394 395 $self->abort("Leftover files in the directory ($full)!\n"); 396 } 397 closedir($dh); 398 399 if ($ENV{T2_KEEP_TEMPDIR}) { 400 print STDERR "# Not removing temp dir: $tempdir\n"; 401 return; 402 } 403 404 my $abort = File::Spec->catfile($self->{+TEMPDIR}, "ABORT"); 405 unlink($abort) if -e $abort; 406 rmdir($tempdir) or warn "Could not remove IPC temp dir ($tempdir)"; 407} 408 4091; 410 411__END__ 412 413=pod 414 415=encoding UTF-8 416 417=head1 NAME 418 419Test2::IPC::Driver::Files - Temp dir + Files concurrency model. 420 421=head1 DESCRIPTION 422 423This is the default, and fallback concurrency model for L<Test2>. This 424sends events between processes and threads using serialized files in a 425temporary directory. This is not particularly fast, but it works everywhere. 426 427=head1 SYNOPSIS 428 429 use Test2::IPC::Driver::Files; 430 431 # IPC is now enabled 432 433=head1 ENVIRONMENT VARIABLES 434 435=over 4 436 437=item T2_KEEP_TEMPDIR=0 438 439When true, the tempdir used by the IPC driver will not be deleted when the test 440is done. 441 442=item T2_TEMPDIR_TEMPLATE='test2-XXXXXX' 443 444This can be used to set the template for the IPC temp dir. The template should 445follow template specifications from L<File::Temp>. 446 447=back 448 449=head1 SEE ALSO 450 451See L<Test2::IPC::Driver> for methods. 452 453=head1 SOURCE 454 455The source code repository for Test2 can be found at 456F<http://github.com/Test-More/test-more/>. 457 458=head1 MAINTAINERS 459 460=over 4 461 462=item Chad Granum E<lt>exodist@cpan.orgE<gt> 463 464=back 465 466=head1 AUTHORS 467 468=over 4 469 470=item Chad Granum E<lt>exodist@cpan.orgE<gt> 471 472=back 473 474=head1 COPYRIGHT 475 476Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>. 477 478This program is free software; you can redistribute it and/or 479modify it under the same terms as Perl itself. 480 481See F<http://dev.perl.org/licenses/> 482 483=cut 484