2020-06-15 19:42:13 +00:00
#!/usr/bin/perl
# License: AGPLv3-or-later
# see files COPYRIGHT, LICENSE, and AGPL-3.0.txt included in canonical repository for details.
# https://k.sfconservancy.org/NPO-Accounting/beancount-tools-in-Perl
use strict;
use warnings;
use autodie qw(:all);
use Getopt::Long;
use File::Spec::Functions;
use File::Temp qw/:mktemp/;
use POSIX qw(mkfifo);
use IPC::Shareable;
2020-08-10 13:37:08 +00:00
use IPC::Run qw(start pump finish timeout);
2020-06-15 19:42:13 +00:00
my $BEANCOUNT_QUERY_CMD = "/usr/bin/bean-query";
my($VERBOSE, $BEANCOUNT_DIR, $LOAD_FILE, $REPOSITORY_URL, $FIFO_DIR) = (0, undef, undef, undef, undef);
GetOptions("verbose=i" => \$VERBOSE, "beancountDir=s" => \$BEANCOUNT_DIR,
"loadFile=s" => \$LOAD_FILE, "repositoryURL=s" => \$REPOSITORY_URL, 'fifoDir=s' => \$FIFO_DIR);
sub UsageAndExit($) {
print STDERR "usage: $0 --loadFile=/path/to/file.beancount --beancountDir=/path/to/beancountdir --fifoDir=/path/to/directory/for/fifos [ --repositoryURL=URL_OF_REPOSITORY --verbose=N ]\n";
print STDERR "\n $_[0]\n";
exit 2;
}
UsageAndExit("/path/to/direcotry/for/fifos must be provided and be an existing directory")
unless (defined $FIFO_DIR and -d $FIFO_DIR);
UsageAndExit("/path/to/file.beancount/path/to/beancountdir must be provided and be an existing directory")
unless (defined $BEANCOUNT_DIR and -d $BEANCOUNT_DIR);
chdir $BEANCOUNT_DIR;
UsageAndExit("/path/to/file.beancount must a relative path to a file that exists in $BEANCOUNT_DIR")
unless (defined $LOAD_FILE and -r $LOAD_FILE);
if (defined $REPOSITORY_URL) {
UsageAndExit("if --repositoryURL is provided, $BEANCOUNT_DIR must be a checkout of a Git repository, but there is no $BEANCOUNT_DIR/.git/config.")
unless (-r '.git/config');
open(my $gitFH, "<", ".git/config");
my $repoString;
while (my $line = <$gitFH>) {
chomp $line;
$repoString = $1
if $line =~ /^\s*url\s*=\s*(\S+)\s*$/;
}
close $gitFH;
UsageAndExit("if --repositoryURL is provided, the checkout found in $BEANCOUNT_DIR must be of that repository, but .git/config does not list a URL.") unless defined $REPOSITORY_URL;
UsageAndExit("if --repositoryURL is provided, the checkout found in $BEANCOUNT_DIR must be of that repository, but it instead appears to be a checkout of $repoString.")
unless ($repoString eq $REPOSITORY_URL);
}
my $glue = 'BeAn';
my %options = (
create => 'yes',
exclusive => 0,
mode => 0600,
destroy => 'yes',
);
my %query;
tie %query, 'IPC::Shareable', $glue, { %options } or
die "server: tie failed for $glue\n";
%query = ();
my %beancountData;
tie %beancountData, 'IPC::Shareable', $glue, { %options } or
die "server: tie failed\n";
2020-08-10 14:35:32 +00:00
my ($currentFormat, $runningBeanQuery, $rbcIn, $rbcOut, $rbcErr);
2020-08-10 13:37:08 +00:00
sub StartRunningBeanQuery {
my($format) = @_;
$format = $currentFormat unless defined $format;
$currentFormat = $format;
my @cmd = ($BEANCOUNT_QUERY_CMD);
2020-08-10 14:46:29 +00:00
push(@cmd, '-f', $format) if defined $format;
2020-08-10 13:37:08 +00:00
push(@cmd, $LOAD_FILE);
2020-08-10 14:35:32 +00:00
$runningBeanQuery = start \@cmd, '<pty<', \$rbcIn, '>pty>', \$rbcOut, '2>', \$rbcErr;
2020-08-10 13:37:08 +00:00
pump $runningBeanQuery until $rbcOut =~ /^\s*beancount\s*\>\s*/m;
}
StartRunningBeanQuery('text');
2020-06-15 19:42:13 +00:00
while (1) {
if (not defined $query{question}) {
2020-06-15 21:42:33 +00:00
print STDERR "No question posed..." if $VERBOSE > 2;
2020-06-15 19:42:13 +00:00
if (defined $query{fifoName}) {
2020-06-15 21:42:33 +00:00
print STDERR "fifo still active; locking to clear it..." if $VERBOSE > 2;
(tied %query)->shlock;
print STDERR "clearing fifo, $query{fifoName}..." if $VERBOSE > 2;
2020-06-15 19:42:13 +00:00
no autodie 'unlink'; unlink($query{fifoName});
2020-06-16 03:40:22 +00:00
%query = ();
2020-06-15 21:42:33 +00:00
(tied %query)->shunlock;
print STDERR "fifo cleared & lock released." if $VERBOSE > 2;
2020-06-15 19:42:13 +00:00
}
2020-06-15 21:42:33 +00:00
print STDERR "sleep for 2 seconds\n" if $VERBOSE > 2;
2020-06-15 19:42:13 +00:00
sleep 2;
next;
2020-08-10 04:02:46 +00:00
} elsif ($query{question} !~ /^[\,\=\~\-\@\w.\s\"\'\_\(\)\<\>\*\.\!]+$/) {
2020-06-15 19:42:13 +00:00
print STDERR "Query string $query{question} looks suspicious, not running beancount query!\n";
(tied %query)->shlock;
2020-06-16 03:37:49 +00:00
$query{fifoName} = mktemp("REJECTED_beancount-query-fifo-this-file-does-not-exist_${$}_XXXXXXXXX");
(tied %query)->shunlock;
2020-06-16 03:40:22 +00:00
sleep 2;
2020-06-16 03:37:49 +00:00
} elsif (defined $query{format} and $query{format} !~ /^(?:csv|text)$/) {
print STDERR "format string $query{format} is not text or csv, not running beancount query!\n";
(tied %query)->shlock;
$query{question} = $query{format} = undef;
2020-06-15 19:42:13 +00:00
$query{fifoName} = mktemp("REJECTED_beancount-query-fifo-this-file-does-not-exist_${$}_XXXXXXXXX");
(tied %query)->shunlock;
} elsif (not defined $query{fifoName}) {
2020-08-10 14:35:32 +00:00
if (defined $query{format} and $query{format} ne 'text') {
print STDERR "format string $query{format} is not supported yet!\n";
(tied %query)->shlock;
$query{question} = $query{format} = undef;
$query{fifoName} = mktemp("REJECTED_beancount-$query{format}_not_supported_${$}_XXXXXXXXX");
(tied %query)->shunlock;
next;
}
2020-06-16 03:40:22 +00:00
print STDERR "Runing query: $query{question}\n" if $VERBOSE > 0;
2020-08-10 13:37:08 +00:00
$rbcOut = "";
2020-08-10 14:35:32 +00:00
$rbcErr = "";
my $ques = $query{question};
$ques =~ s/\n/ /gm;
$rbcIn = "$ques\n";
2020-08-10 13:37:08 +00:00
pump $runningBeanQuery until $rbcOut =~ /^\s*beancount\s*\>\s*/m;
2020-06-15 19:42:13 +00:00
(tied %query)->shlock;
print STDERR "Acquired shlock on tied variable.\n" if $VERBOSE > 1;
$query{fifoName} = undef;
my $cnt = 0;
my $fifoFileName = mktemp(catfile($FIFO_DIR, "beancount-query-fifo_${$}_XXXXXXXXX"));
mkfifo($fifoFileName, 0700);
die "unable to create fifo in $fifoFileName: $!" unless -p $fifoFileName;
print STDERR "Created $fifoFileName..." if $VERBOSE > 1;
$query{fifoName} = $fifoFileName;
(tied %query)->shunlock;
open(my $fifoFH, ">", $fifoFileName);
print STDERR "and beginning write to it." if $VERBOSE > 1;
2020-08-10 14:35:32 +00:00
my($seenSeperator, $prevLine) = (0, "");
2020-08-10 13:37:08 +00:00
foreach my $line (split /\n/, $rbcOut) {
2020-08-10 14:35:32 +00:00
# Occasionally, some of the SELECT statement is printed back to
# $rbcOut. Avoid reproducing this in output by waiting for the line
# of ----'s. Thus, this only works in text mode right now.
unless ($seenSeperator) {
if ($line =~ /^\s*$/) {
$seenSeperator = 1;
} elsif ($currentFormat eq 'text' and $line =~ /^\s*\-+\s*$/) {
$seenSeperator = 1;
}
$prevLine = $line;
next;
}
2020-08-10 13:37:08 +00:00
last if $line =~ /^\s*beancount\s*\>\s*/;
2020-06-15 21:52:23 +00:00
print STDERR "." unless ($cnt++ % 500) or ($VERBOSE <= 1);
2020-08-10 13:37:08 +00:00
print $fifoFH "$line\n";
2020-08-10 14:35:32 +00:00
$prevLine = $line;
2020-06-15 19:42:13 +00:00
}
close $fifoFH;
2020-08-10 13:37:08 +00:00
$rbcOut = "";
2020-08-10 14:35:32 +00:00
$rbcIn = "";
2020-06-15 19:42:13 +00:00
(tied %query)->shlock; $query{question} = undef; (tied %query)->shunlock;
print STDERR "...done! Data now in $fifoFileName\n" if $VERBOSE > 0;
}
}
2020-08-10 13:37:08 +00:00
finish $runningBeanQuery;
2020-06-15 19:42:13 +00:00
###############################################################################
#
# Local variables:
# compile-command: "perl -c bean-query-goofy-daemon.plx"
# perl-indent-level: 2
# End: