File Coverage

blib/lib/fs/Promises.pm
Criterion Covered Total %
statement 110 181 60.7
branch 19 26 73.0
condition 6 14 42.8
subroutine 22 49 44.9
pod 0 24 0.0
total 157 294 53.4


line stmt bran cond sub pod time code
1             package fs::Promises;
2 1     1   534 use v5.24;
  1         4  
3 1     1   5 use warnings;
  1         2  
  1         39  
4 1     1   621 use experimental qw/signatures lexical_subs/;
  1         3750  
  1         5  
5 1         29 no warnings qw/
6             experimental
7             experimental::signatures
8             experimental::lexical_subs
9 1     1   230 /;
  1         3  
10              
11             # Core
12 1     1   5 use File::Spec ();
  1         2  
  1         25  
13              
14 1     1   517 use AnyEvent::XSPromises ();
  1         8554  
  1         23  
15 1     1   496 use POSIX::AtFork ();
  1         521  
  1         77  
16             POSIX::AtFork->add_to_child(sub {
17             IO::AIO::reinit() if $INC{'IO/AIO.pm'};
18             });
19              
20             # Utils
21 1     1   530 use Ref::Util ();
  1         1706  
  1         26  
22 1     1   612 use Hash::Util::FieldHash ();
  1         952  
  1         131  
23              
24             our $VERSION = 0.02;
25              
26 7     7   43 my sub deferred { AnyEvent::XSPromises::deferred() }
27 70     70   195 my sub resolved { AnyEvent::XSPromises::resolved(@_) }
28 0     0   0 my sub rejected { AnyEvent::XSPromises::rejected(@_) }
29              
30 1     1   16 use Exporter 'import';
  1         2  
  1         125  
31             our @EXPORT_OK = qw(
32             open
33             close
34             stat
35             lstat
36              
37             seek
38              
39             fcntl
40             ioctl
41              
42              
43             utime
44             chown
45             chmod
46             truncate
47              
48             unlink
49             link
50             symlink
51             rename
52             copy
53             move
54              
55             readlink
56             realpath
57             mkdir
58             rmdir
59             rmtree
60              
61             slurp
62             readline
63             );
64             my @promise_versions;
65             foreach my $exported ( @EXPORT_OK ) {
66             my $promise_version = "${exported}_promise";
67             push @promise_versions, $promise_version;
68 1     1   7 no strict 'refs';
  1         1  
  1         101  
69             *{$promise_version} = \*{$exported};
70             }
71             push @EXPORT_OK, @promise_versions;
72              
73 1   50 1   7 use constant DEBUG => $ENV{DEBUG_fs_Promises} // 0;
  1         2  
  1         2940  
74 0     0   0 sub TELL { say STDERR sprintf(__PACKAGE__ . ': ' . shift, @_) }
75              
76             Hash::Util::FieldHash::fieldhash my %per_fh_buffer_cache;
77              
78 77 50 0 77   233 my sub _drop_self { shift @_ if @_ > 1 && ($_[0]//'') eq __PACKAGE__; }
      33        
79              
80             my sub lazily_require_aio {
81 3     3   5 state $loaded = do {
82 1         840 require IO::AIO;
83 1         6777 require AnyEvent::AIO;
84 1         361 1;
85             };
86 3         7 return $loaded;
87             }
88              
89             sub open {
90 3     3 0 7050 &_drop_self;
91 3         7 my ($maybe_rel_file, $mode) = @_; # TODO: mode!
92 3         11 lazily_require_aio();
93 3   33     22 $mode ||= IO::AIO::O_RDONLY();
94              
95 3         24 my %symbolic_mode_to_numeric = (
96             '>' => IO::AIO::O_WRONLY() | IO::AIO::O_CREAT(),
97             '>>' => IO::AIO::O_WRONLY() | IO::AIO::O_CREAT() | IO::AIO::O_APPEND(),
98             '<' => IO::AIO::O_RDONLY(),
99             );
100             $mode = $symbolic_mode_to_numeric{$mode}
101 3 50       11 if exists $symbolic_mode_to_numeric{$mode};
102              
103 3         129 my $abs_file = File::Spec->rel2abs($maybe_rel_file); # AIO api requires absolute paths
104 3         15 my $deferred = deferred();
105 3     3   6 IO::AIO::aio_open($abs_file, $mode, 0, sub ($fh=undef) {
  3         287  
  3         10  
106 3 100       11 if ( !$fh ) {
107 1         13 $deferred->reject($!);
108 1         80 return;
109             }
110 2         14 $deferred->resolve($fh);
111 3         224 });
112 3         4281 return $deferred->promise;
113             }
114              
115             my sub _arg_is_fh {
116 0     0   0 &_drop_self;
117 0         0 my $cb = shift;
118 0         0 my $deferred = deferred();
119 0         0 $cb->(@_, sub { $deferred->resolve(@_) });
  0         0  
120 0         0 return $deferred->promise;
121             }
122              
123 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  
124 0     0 0 0 sub seek { lazily_require_aio(); _arg_is_fh(\&IO::AIO::aio_seek, @_) }
  0         0  
125 0     0 0 0 sub fcntl { lazily_require_aio(); _arg_is_fh(\&IO::AIO::aio_fcntl, @_) }
  0         0  
126 0     0 0 0 sub ioctl { lazily_require_aio(); _arg_is_fh(\&IO::AIO::aio_ioctl, @_) }
  0         0  
127              
128             my sub _ensure_globref_or_absolute_path {
129             my ($fh_or_file) = @_;
130             if (
131             Ref::Util::is_globref($fh_or_file)
132             || Ref::Util::is_globref(\$fh_or_file)
133             || Ref::Util::is_ioref($fh_or_file)
134             ) {
135             # Globref/IO object, just return it
136             return $fh_or_file;
137             }
138             # Probably a path -- We need to make it an absolute path per
139             # the AIO API.
140             return File::Spec->rel2abs($fh_or_file);
141             }
142              
143             my sub _arg_is_fh_or_file {
144 0     0   0 &_drop_self;
145 0         0 my $cb = shift;
146 0         0 my $fh_or_maybe_rel_path = shift;
147 0         0 my $fh_or_abs_path = _ensure_globref_or_absolute_path($fh_or_maybe_rel_path);
148 0         0 my $deferred = deferred();
149 0         0 push @_, sub { $deferred->resolve(@_) };
  0         0  
150 0         0 $cb->($fh_or_abs_path, @_);
151 0         0 return $deferred->promise;
152             }
153              
154              
155 0     0 0 0 sub stat { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_stat, @_) }
  0         0  
156 0     0 0 0 sub lstat { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_lstat, @_) }
  0         0  
157 0     0 0 0 sub utime { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_utime, @_) }
  0         0  
158 0     0 0 0 sub chown { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_chown, @_) }
  0         0  
159 0     0 0 0 sub truncate { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_truncate, @_) }
  0         0  
160 0     0 0 0 sub chmod { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_chmod, @_) }
  0         0  
161 0     0 0 0 sub unlink { lazily_require_aio(); _arg_is_fh_or_file(\&IO::AIO::aio_unlink, @_) }
  0         0  
162              
163             my sub _arg_is_two_paths {
164 0     0   0 &_drop_self;
165 0         0 my $cb = shift;
166 0         0 my ($first_path, $second_path) = map File::Spec->rel2abs($_), shift, shift;
167 0         0 my $deferred = deferred();
168 0         0 $cb->($first_path, $second_path, @_, sub { $deferred->resolve(@_) });
  0         0  
169 0         0 return $deferred->promise;
170             }
171              
172 0     0 0 0 sub link { lazily_require_aio(); _arg_is_two_paths(\&IO::AIO::aio_link, @_) }
  0         0  
173 0     0 0 0 sub symlink { lazily_require_aio(); _arg_is_two_paths(\&IO::AIO::aio_symlink, @_) }
  0         0  
174 0     0 0 0 sub rename { lazily_require_aio(); _arg_is_two_paths(\&IO::AIO::aio_rename, @_) }
  0         0  
175 0     0 0 0 sub copy { lazily_require_aio(); _arg_is_two_paths(\&IO::AIO::aio_copy, @_) }
  0         0  
176 0     0 0 0 sub move { lazily_require_aio(); _arg_is_two_paths(\&IO::AIO::aio_move, @_) }
  0         0  
177              
178              
179             my sub _arg_is_single_path {
180 0     0   0 &_drop_self;
181 0         0 my $cb = shift;
182 0         0 my $first_path = File::Spec->rel2abs(shift);
183 0         0 my $deferred = deferred();
184 0         0 $cb->($first_path, @_, sub { $deferred->resolve(@_) });
  0         0  
185 0         0 return $deferred->promise;
186             }
187 0     0 0 0 sub readlink { _arg_is_single_path(\&IO::AIO::aio_readlink, @_) }
188 0     0 0 0 sub realpath { _arg_is_single_path(\&IO::AIO::aio_realpath, @_) }
189 0     0 0 0 sub mkdir { _arg_is_single_path(\&IO::AIO::aio_mkdir, @_) }
190 0     0 0 0 sub rmdir { _arg_is_single_path(\&IO::AIO::aio_rmdir, @_) }
191 0     0 0 0 sub rmtree { _arg_is_single_path(\&IO::AIO::aio_rmtree, @_) }
192              
193             sub slurp {
194 1     1 0 2645 &_drop_self;
195 1         31 my $file = File::Spec->rel2abs(shift);
196 1         5 my $deferred = deferred();
197 1         3 my $buffer = '';
198             IO::AIO::aio_slurp($file, 0, 0, $buffer, sub {
199 1 50   1   225 if ( $_[0] <= 0 ) {
200 0         0 $deferred->reject($!);
201 0         0 return;
202             }
203 1         14 $deferred->resolve($buffer);
204 1         45 });
205 1         10 return $deferred->promise;
206             }
207              
208             sub readline {
209 73     73 0 793 &_drop_self;
210 73         212 my ($fh, $block_size) = @_;
211 73   50     308 $block_size ||= 8192; #(stat $fh)[11] || 1024;
212              
213 73         145 my $eol = $/;
214              
215 73 50       148 if ( !$fh ) {
216 0         0 return rejected("No filehandle provided to readline()");
217             }
218              
219 73         98 my $io = *{$fh}{IO};
  73         179  
220              
221 73   100     423 my $buffer = \($per_fh_buffer_cache{$io} //= '');
222              
223 73         186 my $fileno = fileno($fh);
224 73         130 my $buf_index = length($$buffer);
225 73 100       140 if ( $buf_index ) {
226 70 50       196 my $eol_index = $eol ? index($$buffer, $eol, 0) : -1;
227              
228 70 50       132 if ( $eol_index >= 0 ) {
229 70         86 DEBUG and TELL "fd %d: cached EOL", $fileno;
230             # previous read got multiple lines!
231 70         199 my $line = substr($$buffer, 0, $eol_index + 1, '');
232 70         148 return resolved($line);
233             }
234             }
235              
236 3         20 my $deferred = deferred();
237             sub {
238 4     4   7 my $do_aio_read = __SUB__;
239 4         9 my $this_read_buf = '';
240             IO::AIO::aio_read(
241             $fh,
242             undef, # read offset -- undef means from the fd's offset
243             $block_size, # read size
244             $this_read_buf, # buffer to place the read data into
245             0, # offset in the buffer to start writing from
246             sub {
247 4         356 my ($bytes_read) = @_;
248              
249 4 100       13 if ( !$bytes_read ) {
250             # EOF; return what we have so far
251 2 100       9 if ( $$buffer ) {
252 1         2 DEBUG and TELL "fd %d: EOF, with cached EOL", $fileno;
253 1         11 $deferred->resolve("$$buffer");
254 1         19 $$buffer = '';
255             }
256             else {
257 1         2 DEBUG and TELL "fd %d: EOF", $fileno;
258             # we read nothing, and had nothing buffered. Return undef.
259 1         8 $deferred->resolve(undef);
260             }
261 2         52 return;
262             }
263              
264 2 50       12 $$buffer .= $this_read_buf if $bytes_read;
265              
266 2 100       9 my $eol_index = $eol ? index($$buffer, $eol, $buf_index) : -1;
267              
268 2 100       7 if ( $eol_index >= 0 ) {
269 1         2 DEBUG and TELL "fd %d: EOL", $fileno;
270 1         2 $buf_index = 0;
271 1         4 my $found = substr($$buffer, 0, $eol_index + 1, '');
272 1         6 $deferred->resolve($found);
273 1         32 return;
274             }
275              
276 1         3 $buf_index += $bytes_read;
277              
278             # Not EOF, but not EOL, so do another read:
279 1         4 DEBUG and TELL "fd %d: No EOL or EOF, doing another read", $fileno;
280 1         3 return $do_aio_read->();
281             },
282 4         231 );
283 3         23 }->();
284              
285 3         37 return $deferred->promise;
286             }
287              
288             1;
289             __END__