File Coverage

blib/lib/fs/Promises.pm
Criterion Covered Total %
statement 134 211 63.5
branch 22 38 57.8
condition 6 14 42.8
subroutine 26 54 48.1
pod 0 25 0.0
total 188 342 54.9


line stmt bran cond sub pod time code
1             package fs::Promises;
2 2     2   1100 use v5.24;
  2         13  
3 2     2   10 use warnings;
  2         4  
  2         130  
4 2     2   1871 use experimental qw/signatures lexical_subs/;
  2         7576  
  2         13  
5 2         99 no warnings qw/
6             experimental
7             experimental::signatures
8             experimental::lexical_subs
9 2     2   451 /;
  2         4  
10              
11             # Core
12 2     2   13 use File::Spec ();
  2         3  
  2         46  
13              
14 2     2   25 use Scalar::Util ();
  2         5  
  2         45  
15              
16 2     2   1003 use AnyEvent::XSPromises ();
  2         17128  
  2         49  
17 2     2   968 use POSIX::AtFork ();
  2         1047  
  2         148  
18             POSIX::AtFork->add_to_child(sub {
19             IO::AIO::reinit() if $INC{'IO/AIO.pm'};
20             });
21              
22             # Utils
23 2     2   1427 use Ref::Util ();
  2         6188  
  2         60  
24 2     2   2017 use Hash::Util::FieldHash ();
  2         2822  
  2         331  
25              
26             our $VERSION = 0.04;
27              
28 11     11   70 my sub deferred { AnyEvent::XSPromises::deferred() }
29 85     85   230 my sub resolved { AnyEvent::XSPromises::resolved(@_) }
30 0     0   0 my sub rejected { AnyEvent::XSPromises::rejected(@_) }
31              
32             my sub errno {
33 3     3   36 my $e_num = 0 + $!;
34 3         13 my $e_str = "$!";
35 3         38 return Scalar::Util::dualvar( $e_num, $e_str );
36             }
37              
38 2     2   14 use Exporter 'import';
  2         4  
  2         276  
39             our @EXPORT_OK = qw(
40             open
41             close
42             stat
43             lstat
44              
45             seek
46              
47             fcntl
48             ioctl
49              
50              
51             utime
52             chown
53             chmod
54             truncate
55              
56             unlink
57             link
58             symlink
59             rename
60             copy
61             move
62              
63             readlink
64             realpath
65             mkdir
66             rmdir
67             rmtree
68             scandir
69              
70             slurp
71             readline
72             );
73             my @promise_versions;
74             foreach my $exported ( @EXPORT_OK ) {
75             my $promise_version = "${exported}_promise";
76             push @promise_versions, $promise_version;
77 2     2   14 no strict 'refs';
  2         4  
  2         182  
78             *{$promise_version} = \*{$exported};
79             }
80             push @EXPORT_OK, @promise_versions;
81              
82 2   50 2   11 use constant DEBUG => $ENV{DEBUG_fs_Promises} // 0;
  2         4  
  2         6629  
83 0     0   0 sub TELL { say STDERR sprintf(__PACKAGE__ . ': ' . shift, @_) }
84              
85             Hash::Util::FieldHash::fieldhash my %per_fh_buffer_cache;
86              
87 96 50 0 96   344 my sub _drop_self { shift @_ if @_ > 1 && ($_[0]//'') eq __PACKAGE__; }
      33        
88              
89             my sub lazily_require_aio {
90 5     5   11 state $loaded = do {
91 2         1660 require IO::AIO;
92 2         14381 require AnyEvent::AIO;
93 2         728 1;
94             };
95 5         11 return $loaded;
96             }
97             sub scandir {
98 0     0 0 0 &_drop_self;
99 0         0 my $path = File::Spec->rel2abs(shift);
100 0         0 my $max_req = shift;
101 0         0 my $deferred = AnyEvent::XSPromises::deferred();
102             IO::AIO::aio_scandir($path, $max_req, sub {
103 0 0   0   0 if ( !@_ ) {
104 0         0 $deferred->reject(errno());
105 0         0 return;
106             }
107 0         0 $deferred->resolve(@_);
108 0         0 });
109 0         0 return $deferred->promise;
110             }
111              
112             sub open {
113 3     3 0 7129 &_drop_self;
114 3         8 my ($maybe_rel_file, $mode) = @_; # TODO: mode!
115 3         11 lazily_require_aio();
116 3   33     22 $mode ||= IO::AIO::O_RDONLY();
117              
118 3         27 my %symbolic_mode_to_numeric = (
119             '>' => IO::AIO::O_WRONLY() | IO::AIO::O_CREAT(),
120             '>>' => IO::AIO::O_WRONLY() | IO::AIO::O_CREAT() | IO::AIO::O_APPEND(),
121             '<' => IO::AIO::O_RDONLY(),
122             );
123             $mode = $symbolic_mode_to_numeric{$mode}
124 3 50       11 if exists $symbolic_mode_to_numeric{$mode};
125              
126 3         129 my $abs_file = File::Spec->rel2abs($maybe_rel_file); # AIO api requires absolute paths
127 3         14 my $deferred = deferred();
128 3     3   6 IO::AIO::aio_open($abs_file, $mode, 0, sub ($fh=undef) {
  3         478  
  3         11  
129 3 100       11 if ( !$fh ) {
130 1         5 $deferred->reject(errno());
131 1         83 return;
132             }
133 2         17 $deferred->resolve($fh);
134 3         287 });
135 3         4325 return $deferred->promise;
136             }
137              
138             my sub _arg_is_fh {
139 0     0   0 my $cb = shift;
140 0         0 &_drop_self;
141 0         0 my $deferred = deferred();
142 0 0       0 $cb->(@_, sub { $_[0] < 0 ? $deferred->reject(errno()) : $deferred->resolve(@_) });
  0         0  
143 0         0 return $deferred->promise;
144             }
145              
146 0     0 0 0 sub close { lazily_require_aio(); _arg_is_fh(\&IO::AIO::aio_close, @_) } # Don't use unless you know what you are getting into.
  0         0  
147 0     0 0 0 sub seek { lazily_require_aio(); _arg_is_fh(\&IO::AIO::aio_seek, @_) }
  0         0  
148 0     0 0 0 sub fcntl { lazily_require_aio(); _arg_is_fh(\&IO::AIO::aio_fcntl, @_) }
  0         0  
149 0     0 0 0 sub ioctl { lazily_require_aio(); _arg_is_fh(\&IO::AIO::aio_ioctl, @_) }
  0         0  
150              
151             my sub _ensure_globref_or_absolute_path {
152             my ($fh_or_file) = @_;
153             if (
154             Ref::Util::is_globref($fh_or_file)
155             || Ref::Util::is_globref(\$fh_or_file)
156             || Ref::Util::is_ioref($fh_or_file)
157             ) {
158             # Globref/IO object, just return it
159             return $fh_or_file;
160             }
161             # Probably a path -- We need to make it an absolute path per
162             # the AIO API.
163             return File::Spec->rel2abs($fh_or_file);
164             }
165              
166             my sub _arg_is_fh_or_file {
167 0     0   0 my $cb = shift;
168 0         0 &_drop_self;
169 0         0 my $fh_or_maybe_rel_path = shift;
170 0         0 my $fh_or_abs_path = _ensure_globref_or_absolute_path($fh_or_maybe_rel_path);
171 0         0 my $deferred = deferred();
172 0 0       0 push @_, sub { $_[0] < 0 ? $deferred->reject(errno()) : $deferred->resolve(@_) };
  0         0  
173 0         0 $cb->($fh_or_abs_path, @_);
174 0         0 return $deferred->promise;
175             }
176              
177             my sub _wrap_stat_and_lstat {
178 2     2   5 my $cb = shift;
179 2         7 &_drop_self;
180 2         5 my $fh_or_maybe_rel_path = shift;
181 2         7 my $fh_or_abs_path = _ensure_globref_or_absolute_path($fh_or_maybe_rel_path);
182 2         10 my $deferred = deferred();
183             push @_, sub {
184 2         237 my $stat_status = shift;
185 2 100       11 if ( $stat_status ) {
186             # non-zero status for stat; the call failed, so the pseudo-handle _ will hold nothing
187             # of interest
188 1         7 $deferred->reject(errno());
189 1         89 return;
190             }
191              
192             # Get the cached (l)stat results (calling stat or lstat here doesn't matter, since
193             # it just gets whatever is cached in _):
194 1         11 my $stat_results = [ stat(_) ];
195 1         10 $deferred->resolve($stat_results);
196 2         13 };
197 2         246 $cb->($fh_or_abs_path, @_);
198 2         4404 return $deferred->promise;
199             }
200              
201              
202 2     2 0 2238 sub stat { lazily_require_aio(); _wrap_stat_and_lstat(\&IO::AIO::aio_stat, @_) }
  2         7  
203 0     0 0 0 sub lstat { lazily_require_aio(); _wrap_stat_and_lstat(\&IO::AIO::aio_lstat, @_) }
  0         0  
204 0     0 0 0 sub utime { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_utime, @_) }
  0         0  
205 0     0 0 0 sub chown { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_chown, @_) }
  0         0  
206 0     0 0 0 sub truncate { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_truncate, @_) }
  0         0  
207 0     0 0 0 sub chmod { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_chmod, @_) }
  0         0  
208 0     0 0 0 sub unlink { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_unlink, @_) }
  0         0  
209              
210             my sub _arg_is_two_paths {
211 0     0   0 my $cb = shift;
212 0         0 &_drop_self;
213 0         0 my ($first_path, $second_path) = map File::Spec->rel2abs($_), shift, shift;
214 0         0 my $deferred = deferred();
215             $cb->($first_path, $second_path, @_, sub {
216 0 0       0 $_[0] < 0 ? $deferred->reject(errno()) : $deferred->resolve(@_)
217 0         0 });
218 0         0 return $deferred->promise;
219             }
220              
221 0     0 0 0 sub link { lazily_require_aio(); _arg_is_two_paths(\&IO::AIO::aio_link, @_) }
  0         0  
222 0     0 0 0 sub symlink { lazily_require_aio(); _arg_is_two_paths(\&IO::AIO::aio_symlink, @_) }
  0         0  
223 0     0 0 0 sub rename { lazily_require_aio(); _arg_is_two_paths(\&IO::AIO::aio_rename, @_) }
  0         0  
224 0     0 0 0 sub copy { lazily_require_aio(); _arg_is_two_paths(\&IO::AIO::aio_copy, @_) }
  0         0  
225 0     0 0 0 sub move { lazily_require_aio(); _arg_is_two_paths(\&IO::AIO::aio_move, @_) }
  0         0  
226              
227              
228             my sub _arg_is_single_path {
229 0     0   0 my $cb = shift;
230 0         0 &_drop_self;
231 0         0 my $first_path = File::Spec->rel2abs(shift);
232 0         0 my $deferred = deferred();
233             $cb->($first_path, @_, sub {
234 0 0       0 $_[0] < 0 ? $deferred->reject(errno()) : $deferred->resolve(@_)
235 0         0 });
236 0         0 return $deferred->promise;
237             }
238 0     0 0 0 sub readlink { _arg_is_single_path(\&IO::AIO::aio_readlink, @_) }
239 0     0 0 0 sub realpath { _arg_is_single_path(\&IO::AIO::aio_realpath, @_) }
240 0     0 0 0 sub mkdir { _arg_is_single_path(\&IO::AIO::aio_mkdir, @_) }
241 0     0 0 0 sub rmdir { _arg_is_single_path(\&IO::AIO::aio_rmdir, @_) }
242 0     0 0 0 sub rmtree { _arg_is_single_path(\&IO::AIO::aio_rmtree, @_) }
243              
244             sub slurp {
245 3     3 0 8771 &_drop_self;
246 3         72 my $file = File::Spec->rel2abs(shift);
247 3         10 my $deferred = deferred();
248 3         6 my $buffer = '';
249             IO::AIO::aio_slurp($file, 0, 0, $buffer, sub {
250 3 100   3   461 if ( $_[0] < 0 ) { # will be 0 if the file was empty
251 1         5 $deferred->reject(errno());
252 1         27 return;
253             }
254 2         19 $deferred->resolve($buffer);
255 3         122 });
256 3         33 return $deferred->promise;
257             }
258              
259             sub readline {
260 88     88 0 909 &_drop_self;
261 88         277 my ($fh, $block_size) = @_;
262 88   50     388 $block_size ||= 8192; #(stat $fh)[11] || 1024;
263              
264 88         180 my $eol = $/;
265              
266 88 50       175 if ( !$fh ) {
267 0         0 return rejected("No filehandle provided to readline()");
268             }
269              
270 88         109 my $io = *{$fh}{IO};
  88         225  
271              
272 88   100     399 my $buffer = \($per_fh_buffer_cache{$io} //= '');
273              
274 88         214 my $fileno = fileno($fh);
275 88         157 my $buf_index = length($$buffer);
276 88 100       191 if ( $buf_index ) {
277 85 50       223 my $eol_index = $eol ? index($$buffer, $eol, 0) : -1;
278              
279 85 50       194 if ( $eol_index >= 0 ) {
280 85         97 DEBUG and TELL "fd %d: cached EOL", $fileno;
281             # previous read got multiple lines!
282 85         246 my $line = substr($$buffer, 0, $eol_index + 1, '');
283 85         166 return resolved($line);
284             }
285             }
286              
287 3         9 my $deferred = deferred();
288             sub {
289 4     4   8 my $do_aio_read = __SUB__;
290 4         9 my $this_read_buf = '';
291             IO::AIO::aio_read(
292             $fh,
293             undef, # read offset -- undef means from the fd's offset
294             $block_size, # read size
295             $this_read_buf, # buffer to place the read data into
296             0, # offset in the buffer to start writing from
297             sub {
298 4         430 my ($bytes_read) = @_;
299              
300 4 100       14 if ( !$bytes_read ) {
301             # EOF; return what we have so far
302 2 100       8 if ( $$buffer ) {
303 1         3 DEBUG and TELL "fd %d: EOF, with cached EOL", $fileno;
304 1         11 $deferred->resolve("$$buffer");
305 1         18 $$buffer = '';
306             }
307             else {
308 1         3 DEBUG and TELL "fd %d: EOF", $fileno;
309             # we read nothing, and had nothing buffered. Return undef.
310 1         10 $deferred->resolve(undef);
311             }
312 2         60 return;
313             }
314              
315 2 50       11 $$buffer .= $this_read_buf if $bytes_read;
316              
317 2 100       8 my $eol_index = $eol ? index($$buffer, $eol, $buf_index) : -1;
318              
319 2 100       6 if ( $eol_index >= 0 ) {
320 1         3 DEBUG and TELL "fd %d: EOL", $fileno;
321 1         2 $buf_index = 0;
322 1         4 my $found = substr($$buffer, 0, $eol_index + 1, '');
323 1         7 $deferred->resolve($found);
324 1         33 return;
325             }
326              
327 1         3 $buf_index += $bytes_read;
328              
329             # Not EOF, but not EOL, so do another read:
330 1         2 DEBUG and TELL "fd %d: No EOL or EOF, doing another read", $fileno;
331 1         4 return $do_aio_read->();
332             },
333 4         227 );
334 3         20 }->();
335              
336 3         45 return $deferred->promise;
337             }
338              
339             1;
340             __END__