File Coverage

blib/lib/fs/Promises.pm
Criterion Covered Total %
statement 134 201 66.6
branch 22 36 61.1
condition 8 14 57.1
subroutine 26 52 50.0
pod 0 24 0.0
total 190 327 58.1


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