#!/usr/bin/env perl
use strict;
no strict qw(refs);
use threads;
use threads::shared;
use Carp;
local $SIG{'__WARN__'} = \&Carp::cluck;
use Fuse qw(:all);
use Fcntl qw(:mode);
use POSIX;
use IO::Poll qw(POLLIN);
use Time::HiRes qw(sleep);
use Getopt::Long;
# $fsel_open_mask is used to limit the number of opens to 1 per file. This
# uses the file index (0-F) as $fh, as poll support requires a unique handle
# per open file. Lifting this would require more complete open file
# management.
my $fsel_open_mask :shared = 0;
# Maximum "file" size.
use constant FSEL_CNT_MAX => 10;
use constant FSEL_FILES => 16;
# Used only as a lock for $fsel_poll_notify_mask and @fsel_cnt.
my $fsel_mutex :shared;
# Mask indicating what FDs have poll notifications waiting.
my $fsel_poll_notify_mask :shared = 0;
# Poll notification handles.
my @fsel_poll_handle :shared;
# Number of bytes for each "file".
my @fsel_cnt :shared;
# Initialize all byte counts.
map { $fsel_cnt[$_] = 0 } (0 .. (FSEL_FILES - 1));
sub fsel_path_index {
my ($path) = @_;
print 'called ', (caller(0))[3], "\n";
return -1 if $path !~ m{^/([0-9A-F])$};
return hex($1);
}
sub fsel_getattr {
my ($path) = @_;
print 'called ', (caller(0))[3], "\n";
my @stbuf = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
if ($path eq '/') {
@stbuf[2, 3] = (S_IFDIR | 0555, 2);
return @stbuf;
}
my $idx = fsel_path_index($path);
return -&ENOENT if $idx < 0;
@stbuf[2, 3, 7] = (S_IFREG | 0444, 1, $fsel_cnt[$idx]);
return @stbuf;
}
sub fsel_readdir {
my ($path, $offset) = @_;
print 'called ', (caller(0))[3], "\n";
return -&ENOENT if $path ne '/';
return('.', '..', map { sprintf('%X', $_) } (0 .. (FSEL_FILES - 1)), 0);
}
sub fsel_open {
my ($path, $flags, $info) = @_;
print 'called ', (caller(0))[3], "\n";
my $idx = fsel_path_index($path);
return -&ENOENT if $idx < 0;
return -&EACCES if $flags & O_ACCMODE != O_RDONLY;
return -&EBUSY if $fsel_open_mask & (1 << $idx);
$fsel_open_mask |= (1 << $idx);
# fsel files are nonseekable somewhat pipe-like files which get filled
# up periodically by the producer thread, and consumed on read. Tell
# FUSE to do this right.
@{$info}{'direct_io', 'nonseekable'} = (1, 1);
return (0, $idx);
}
sub fsel_release {
my ($path, $flags, $fh) = @_;
print 'called ', (caller(0))[3], "\n";
$fsel_open_mask &= ~(1 << $fh);
return 0;
}
sub fsel_read {
my ($path, $size, $offset, $fh) = @_;
print 'called ', (caller(0))[3], "\n";
lock($fsel_mutex);
if ($fsel_cnt[$fh] < $size) {
$size = $fsel_cnt[$fh];
}
printf("READ \%X transferred=\%u cnt=\%u\n", $fh, $size, $fsel_cnt[$fh]);
$fsel_cnt[$fh] -= $size;
return(chr($fh) x $size);
}
our $polled_zero :shared = 0;
sub fsel_poll {
my ($path, $ph, $revents, $fh) = @_;
print 'called ', (caller(0))[3], ", path = \"$path\", fh = $fh, revents = $revents\n";
lock($fsel_mutex);
if ($ph) {
my $oldph = $fsel_poll_handle[$fh];
pollhandle_destroy($oldph) if $oldph;
$fsel_poll_notify_mask |= (1 << $fh);
$fsel_poll_handle[$fh] = $ph;
}
if ($fsel_cnt[$fh]) {
$revents |= POLLIN;
printf("POLL \%X cnt=\%u polled_zero=\%u\n", $fh, $fsel_cnt[$fh],
$polled_zero);
$polled_zero = 0;
}
else {
$polled_zero++;
}
return(0, $revents);
}
sub fsel_producer {
print 'called ', (caller(0))[3], "\n";
local $SIG{'KILL'} = sub { threads->exit(); };
my ($tv, $idx, $nr) = (0.25, 0, 1);
while (1) {
{
my ($i, $t);
lock($fsel_mutex);
# This is the main producer loop which is executed every 250
# msec. On each iteration, it adds one byte to 1, 2 or 4 files
# and sends a poll notification if a poll handle is present.
for (($i, $t) = (0, $idx); $i < $nr; $i++,
$t = (($t + int(FSEL_FILES / $nr)) % FSEL_FILES)) {
next if $fsel_cnt[$t] == FSEL_CNT_MAX;
$fsel_cnt[$t]++;
if ($fsel_poll_notify_mask & (1 << $t)) {
printf("NOTIFY \%X\n", $t);
my $ph = $fsel_poll_handle[$t];
notify_poll($ph);
pollhandle_destroy($ph);
$fsel_poll_notify_mask &= ~(1 << $t);
$fsel_poll_handle[$t] = undef;
}
}
$idx = ($idx + 1) % FSEL_FILES;
if ($idx == 0) {
# Cycle through 1, 2 and 4.
$nr = ($nr * 2) % 7;
}
}
sleep($tv);
}
}
croak("Fuse doesn't have poll") unless Fuse::fuse_version() >= 2.8;
my %fuseargs = (
'getattr' => 'main::fsel_getattr',
'readdir' => 'main::fsel_readdir',
'open' => 'main::fsel_open',
'release' => 'main::fsel_release',
'read' => 'main::fsel_read',
'poll' => 'main::fsel_poll',
);
GetOptions(
'use-threads' => sub {
print STDERR "Warning: Fuse currently has bugs related to threading which may cause misbehavior\n";
$fuseargs{'threaded'} = 1;
},
'debug' => sub {
$fuseargs{'debug'} = 1;
}
) || croak("Malformed options passed");
$fuseargs{'mountpoint'} = $ARGV[0];
my $thread = threads->create(\&fsel_producer);
Fuse::main(%fuseargs);
$thread->kill('KILL');
$thread->join();