Compare commits

...

10 commits

Author SHA1 Message Date
Bradley M. Kuhn
a5967d4cf9
Glue for IPC now can be overridden via BeancountQueryInitialize()
Allow a single optional parameter to BeancountQueryInitialize() that
overrides the default glue used for the IPC::Sharable glue.
2021-12-17 10:31:09 -08:00
Bradley M. Kuhn
f43e0000d1
Add additional debug output. 2021-01-06 11:29:33 -08:00
Bradley M. Kuhn
20ac3c5ef7
Abandon client question on timeout before die()'ing.
The server would lock up in cases where a client left a question
pending and die()'d due to this timeout.  So, before die()'ing, clear
the question.
2021-01-06 11:29:03 -08:00
Bradley M. Kuhn
10d809c180
Restore support for csv output format.
I took a shortcut on some previous changes and it led to only text
format being supported.  These changes bring back support for csv.
2020-09-25 10:49:34 -07:00
Bradley M. Kuhn
ee82738f0a
Header line regex was not quite right.
Note that the header line can look like this:

 --  ------ ---

… and this regex should match it.
2020-09-23 17:07:16 -07:00
Bradley M. Kuhn
bd38cf6198
More verbosity around the FIFO creation. 2020-09-23 16:46:11 -07:00
Bradley M. Kuhn
c4e5664bb5
Be exceedingly less strict on the question format.
This may ultimately be a security problem; I wanted to filter the
question for only characters that are valid in bean-query, and at
some point, the right move is to look up what characters in the
bean-query parser that are allowed.  However, I kept running into
problems of finding new characters and this was an easier hack.
2020-09-23 16:45:01 -07:00
Bradley M. Kuhn
72559aa8d6
Support Git repository and update it from upstream.
I don't remember what the --repositoryURL option was supposed to do,
but instead I now assume that the it's a Git repository if you give
--branchName and make a copy, and pull from upstream and reload when
there are updates.
2020-08-30 20:14:54 -07:00
Bradley M. Kuhn
ce4c406fe6
Interactive command-Line client using daemon. 2020-08-30 20:14:38 -07:00
Bradley M. Kuhn
e1857dc63f
Add getAllOutput option to query tied variable.
The separator ditching was really a hack that I made to make
unintended queries work, but generally it's relied upon by other
scripts that I use, so I've herein created an option for getting all
the output.
2020-08-30 20:12:20 -07:00
3 changed files with 207 additions and 47 deletions

57
bean-query-cli.plx Executable file
View file

@ -0,0 +1,57 @@
#!/usr/bin/perl
use strict;
use warnings;
use Term::ReadLine;
use Term::ReadKey;
use File::Spec::Functions;
use IPC::Shareable;
require 'bean-query-daemon-lib.pl';
my $term = new Term::ReadLine 'bean-query-d';
my $prompt = 'bqd> ';
my $dir = $ENV{HOME};
$dir = "~$ENV{USER}" if not defined $dir and defined $ENV{USER};
$dir = "~$ENV{LOGNAME}" if not defined $dir and defined $ENV{LOGNAME};
my $pager = $ENV{PAGER} || '/usr/bin/less';
my ($isPaging, $saveOut) = (0, undef);
sub startpage {
return if $isPaging;
open($saveOut, '>&', STDOUT);
open(STDOUT, "|-", $pager);
$isPaging = 1;
}
sub endpage {
return unless $isPaging;
close(STDOUT);
open(STDOUT, '>&', $saveOut);
$isPaging = 0;
}
BeancountQueryInitialize();
my $histfile = catfile($dir, '.bean-shell-history');
$term->ReadHistory($histfile);
my $cmd = "";
while ( (defined ($cmd = $term->readline($prompt))) and $cmd ne 'exit') {
next if $cmd =~ /^\s*$/;
chomp $cmd;
$term->AddHistory($cmd);
$term->WriteHistory($histfile);
my $fileName = BeancountQuerySubmit($cmd, 'text', 1);
open(my $fh, "<", $fileName);
my($beanOutput, $lineCount) = ("", 0);
my($wchar, $hchar, $wpixels, $hpixels) = GetTerminalSize();
while (my $line = <$fh>) { $beanOutput .= $line; $lineCount++; print STDERR ".";}
close $fh;
BeancountQueryComplete();
startpage() if ($lineCount >= $hchar);
print $beanOutput;
endpage();
}
print "\n";

View file

@ -5,18 +5,22 @@
use strict;
use warnings;
use IPC::Shareable;
our %BEANCOUNT_QUERY;
my $IPC_GLUE = 'BeAn';
sub BeancountQueryInitialize {
sub BeancountQueryInitialize (;$) {
$IPC_GLUE = $_[0] if (defined $_[0]);
die "Glue must be exactly four characters" unless defined $IPC_GLUE and length($IPC_GLUE) == 4;
my %options = (create => 0, exclusive => 0, mode => 0600, destroy => 0);
tie %BEANCOUNT_QUERY, 'IPC::Shareable', $IPC_GLUE, { %options } or
die "BEANCOUNT_QUERY: tie failed: is the goffy beancount server running?\n";
}
sub BeancountQuerySubmit($;$) {
my($question, $format) = @_;
sub BeancountQuerySubmit($;$$) {
my($question, $format, $getAllOutput) = @_;
while (defined $BEANCOUNT_QUERY{fifoName} or defined $BEANCOUNT_QUERY{question}) { sleep 1; }
(tied %BEANCOUNT_QUERY)->shlock;
if (defined $BEANCOUNT_QUERY{fifoName} or defined $BEANCOUNT_QUERY{question}) {
@ -28,15 +32,21 @@ sub BeancountQuerySubmit($;$) {
}
$BEANCOUNT_QUERY{question} = $question;
$BEANCOUNT_QUERY{format} = $format if defined $format;
$BEANCOUNT_QUERY{getAllOutput} = $getAllOutput if defined $getAllOutput;
(tied %BEANCOUNT_QUERY)->shunlock;
my $cnt = 0;
while (not defined $BEANCOUNT_QUERY{fifoName}) {
sleep 1;
die "Unable to initiate query to beancount server\n" if ($cnt++ >= (5 * 60));
if ($cnt++ >= (5 * 60)) {
(tied %BEANCOUNT_QUERY)->shlock;
$BEANCOUNT_QUERY{question} = undef;
(tied %BEANCOUNT_QUERY)->shunlock;
die "Unable to initiate query to beancount server\n" ;
}
}
unless (-p $BEANCOUNT_QUERY{fifoName}) {
(tied %BEANCOUNT_QUERY)->shlock;
$BEANCOUNT_QUERY{question} = $BEANCOUNT_QUERY{format} = undef;
$BEANCOUNT_QUERY{question} = $BEANCOUNT_QUERY{format} = $BEANCOUNT_QUERY{getAllOutput} = undef;
(tied %BEANCOUNT_QUERY)->shunlock;
die "Ceci n'est pas une pipe: BEANCOUNT_QUERY{fifoName}, $BEANCOUNT_QUERY{fifoName}:$!"
}

View file

@ -7,27 +7,34 @@ use strict;
use warnings;
use autodie qw(:all);
use sigtrap 'handler' => \&CleanupEvertything, 'normal-signals', 'stack-trace' => 'error-signals';
use Carp;
use Getopt::Long;
use File::Spec::Functions;
use File::Temp qw/:mktemp/;
use File::Temp qw/:mktemp tempdir/;
use Git::Repository 'Log';
use POSIX qw(mkfifo);
use IPC::Shareable;
use Expect;
my $RSYNC_CMD = '/usr/bin/rsync';
# We have to set the PAGER to a passthrough text program to assure that
# output does not get paused
$ENV{PAGER} = "/usr/bin/cat";
my $BEANCOUNT_QUERY_CMD = "/usr/bin/bean-query";
my($VERBOSE, $BEANCOUNT_DIR, $LOAD_FILE, $REPOSITORY_URL, $FIFO_DIR) = (0, undef, undef, undef, undef);
my($VERBOSE, $BEANCOUNT_DIR, $LOAD_FILE, $BRANCH_NAME, $FIFO_DIR, $CLEAR_FILES, $GLUE) = (0, undef, undef, undef, undef, 1, 'BeAn');
GetOptions("verbose=i" => \$VERBOSE, "beancountDir=s" => \$BEANCOUNT_DIR,
"loadFile=s" => \$LOAD_FILE, "repositoryURL=s" => \$REPOSITORY_URL, 'fifoDir=s' => \$FIFO_DIR);
GetOptions("verbose=i" => \$VERBOSE, "beancountDir=s" => \$BEANCOUNT_DIR, 'glue=s' => \$GLUE, 'clearFiles!', \$CLEAR_FILES,
"loadFile=s" => \$LOAD_FILE, "branchName=s" => \$BRANCH_NAME, '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 "usage: $0 --loadFile=/path/to/file.beancount --beancountDir=/path/to/beancountdir --fifoDir=/path/to/directory/for/fifos [ --glue=FOUR_CHAR_STRING --branchName=BRANCH_NAME --verbose=N ]\n";
print STDERR "\n $_[0]\n";
exit 2;
}
@ -41,23 +48,64 @@ 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');
UsageAndExit("glue must be at exactly four characters")
unless (defined $GLUE and length($GLUE) == 4);
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($tempRepository, $tempRepositoryDirectory);
sub CleanupEvertything {
$tempRepository = undef if (defined $tempRepository);
$tempRepositoryDirectory = undef if (defined $tempRepositoryDirectory);
StopRunningBeanQuery();
croak @_;
}
if (defined $BRANCH_NAME) {
my $absGitRepositoryDirectory = File::Spec->rel2abs( $BEANCOUNT_DIR );
$tempRepositoryDirectory = tempdir('beancountquerygoofydaemongit_' . $$ . '_XXXXXXXXXXX',
TMPDIR => 1, CLEANUP => 1);
print STDERR "Copy Git repository to $tempRepositoryDirectory...." if $VERBOSE > 2;
system($RSYNC_CMD, '-Ha', "$absGitRepositoryDirectory/", "$tempRepositoryDirectory/");
print STDERR "copy completed.\n" if $VERBOSE > 2;
$tempRepository = Git::Repository->new( work_tree => $tempRepositoryDirectory );
chdir $tempRepositoryDirectory;
$tempRepository->run(clean => '-fx', { quiet => 1 });
$tempRepository->run(reset => '--hard', { quiet => 1 });
$tempRepository->run(clean => '-fx', { quiet => 1 });
$tempRepository->run(checkout => '$BRANCH_NAME', { quiet => 1 });
}
sub CheckUpstreamAndPull {
# Returns true iff. a pull was required and the files have changed.
return 0 unless (defined $tempRepository);
my $options = { quiet => 1 };
if ($VERBOSE > 5) {
print STDERR "...clean & git pull...";
$options = {};
}
print STDERR "...check if upstream Git changed..." if $VERBOSE > 5;
my $updateOutput = $tempRepository->run(remote => 'update', $options);
print "$updateOutput" if defined $updateOutput and $updateOutput !~ /^\s*$/ and $VERBOSE > 5;
my $curRev = $tempRepository->run('rev-parse' => '@');
my $remoteRev = $tempRepository->run('rev-parse' => '@{u}');
my $baseRev = $tempRepository->run('merge-base' => '@', '@{u}');
print STDERR "...$curRev is current, $remoteRev is remote Rev $baseRev is base...\n" if $VERBOSE > 6;
if ($curRev eq $remoteRev) {
print STDERR "no change..." if $VERBOSE > 5;
return 0;
} elsif ($curRev eq $baseRev) {
$tempRepository->run(clean => '-fx', $options);
$tempRepository->run(reset => '--hard', $options);
$tempRepository->run(clean => '-fx', $options);
my $pullOutput = $tempRepository->run('pull');
print STDERR "\nPerformed pull since remote updated:\n $pullOutput\n" if ($VERBOSE > 0);
return 1;
} else {
CleanupEvertything();
die("our local Git has $curRev, upstream is at $remoteRev, and the base is $baseRev " .
"so give up entirely on trying to make this work.");
}
}
my $glue = 'BeAn';
my %options = (
create => 'yes',
exclusive => 0,
@ -65,13 +113,13 @@ my %options = (
destroy => 'yes',
);
my %query;
tie %query, 'IPC::Shareable', $glue, { %options } or
die "server: tie failed for $glue\n";
tie %query, 'IPC::Shareable', $GLUE, { %options } or
die "server: tie failed for $GLUE\n";
%query = ();
my %beancountData;
tie %beancountData, 'IPC::Shareable', $glue, { %options } or
tie %beancountData, 'IPC::Shareable', $GLUE, { %options } or
die "server: tie failed\n";
my ($currentFormat, $runningBeanQuery);
@ -81,37 +129,65 @@ sub StartRunningBeanQuery {
$format = $currentFormat unless defined $format;
$currentFormat = $format;
if (defined $tempRepository) {
print STDERR "Clearing temp files from repository..." if $VERBOSE > 4;
$tempRepository->run(clean => '-fx', { quiet => 1 });
$tempRepository->run(reset => '--hard', { quiet => 1 });
$tempRepository->run(clean => '-fx', { quiet => 1 });
} elsif ($CLEAR_FILES) {
my(@findCmd) = ("/usr/bin/find", '.', '-name', '*.picklecache');
if ($VERBOSE > 4) {
print STDERR "Cleared the following picklecache files (none listed means none existed)...\n";
push(@findCmd, '-ls');
}
push(@findCmd, '-exec', '/usr/bin/rm', '-f', '{}', ';');
system(@findCmd);
print STDERR "...done clearing picklecache files.\n" if ($VERBOSE > 4);
}
my @cmd = ($BEANCOUNT_QUERY_CMD);
push(@cmd, '-f', $format) if defined $format;
push(@cmd, $LOAD_FILE);
print STDERR "Starting beancount..." if $VERBOSE > 4;
$runningBeanQuery = Expect->spawn(@cmd);
print STDERR "...spawned & loading data..." if $VERBOSE > 4;
$runningBeanQuery->log_stdout(0);
$runningBeanQuery->expect(undef, -re => '^\s*beancount\s*\>\s*')
or die("Unable to find beancount prompt, output was instead: ".
$runningBeanQuery->before() . $runningBeanQuery->after());
print STDERR "now ready." if $VERBOSE > 4;
print STDERR "Beancount started with output of:\n", $runningBeanQuery->before(),
$runningBeanQuery->match(), $runningBeanQuery->after(), "\n"
if ($VERBOSE > 3);
}
sub StopRunningBeanQuery {
return if not defined $runningBeanQuery;
$runningBeanQuery->send("exit\n");
$runningBeanQuery->soft_close();
}
CheckUpstreamAndPull();
StartRunningBeanQuery('text');
print STDERR "Beancount started. Main loop begins." if $VERBOSE > 0;
while (1) {
if (not defined $query{question}) {
print STDERR "No question posed..." if $VERBOSE > 2;
print STDERR "No question posed..." if $VERBOSE > 5;
if (CheckUpstreamAndPull()) {
StopRunningBeanQuery();
StartRunningBeanQuery();
}
if (defined $query{fifoName}) {
print STDERR "fifo still active; locking to clear it..." if $VERBOSE > 2;
print STDERR "fifo still active; locking to clear it..." if $VERBOSE > 5;
(tied %query)->shlock;
print STDERR "clearing fifo, $query{fifoName}..." if $VERBOSE > 2;
print STDERR "clearing fifo, $query{fifoName}..." if $VERBOSE > 5;
no autodie 'unlink'; unlink($query{fifoName});
%query = ();
(tied %query)->shunlock;
print STDERR "fifo cleared & lock released." if $VERBOSE > 2;
print STDERR "fifo cleared & lock released." if $VERBOSE > 5;
}
print STDERR "sleep for 2 seconds\n" if $VERBOSE > 2;
print STDERR "sleep for 2 seconds\n" if $VERBOSE > 5;
sleep 2;
next;
} elsif ($query{question} !~ /^[\,\=\~\-\@\w.\s\"\'\_\(\)\<\>\*\.\!]+$/) {
# } elsif ($query{question} !~ /^[\,\=\~\-\@\w.\s\"\'\_\(\)\<\>\*\.\!\^\:\$\|]+$/) {
} elsif ($query{question} !~ /^[\s\S]+$/) {
print STDERR "Query string $query{question} looks suspicious, not running beancount query!\n";
(tied %query)->shlock;
$query{fifoName} = mktemp("REJECTED_beancount-query-fifo-this-file-does-not-exist_${$}_XXXXXXXXX");
@ -124,13 +200,26 @@ while (1) {
$query{fifoName} = mktemp("REJECTED_beancount-query-fifo-this-file-does-not-exist_${$}_XXXXXXXXX");
(tied %query)->shunlock;
} elsif (not defined $query{fifoName}) {
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;
if (defined $query{format}) {
unless ($query{format} =~ /^(?:text|csv)$/) {
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;
}
if ($currentFormat ne $query{format}) {
$runningBeanQuery->send("set format $query{format}\n");
$runningBeanQuery->expect(undef, 'beancount>') # *Don't* use regex here!
or die("Unable to find beancount prompt, output was instead: ".
$runningBeanQuery->before() . $runningBeanQuery->after());
$currentFormat = $query{format};
print STDERR "Switched formats to the $currentFormat..." if $VERBOSE > 3;
my $rbcBefore = $runningBeanQuery->before();
die("Unable to change format to $currentFormat")
unless $rbcBefore =~ /format\s*:\s*$currentFormat/ixm;
}
}
print STDERR "Runing query: $query{question}\n" if $VERBOSE > 0;
my $ques = $query{question};
@ -141,27 +230,31 @@ while (1) {
or die("Unable to find beancount prompt, output was instead: ".
$runningBeanQuery->before() . $runningBeanQuery->after());
(tied %query)->shlock;
print STDERR "Acquired shlock on tied variable.\n" if $VERBOSE > 1;
print STDERR "Acquired shlock on tied variable.\n" if $VERBOSE > 2;
$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;
print STDERR "Created $fifoFileName..." if $VERBOSE > 2;
$query{fifoName} = $fifoFileName;
my $getAllOutput = $query{getAllOutput};
(tied %query)->shunlock;
print STDERR "unlocked tied query..." if $VERBOSE > 2;
open(my $fifoFH, ">", $fifoFileName);
print STDERR "and beginning write to it." if $VERBOSE > 1;
my($seenSeperator, $prevLine) = (0, "");
print STDERR "and began write to fifo." if $VERBOSE > 2;
my($seenSeperator, $prevLine) = ( ((defined $getAllOutput) and $getAllOutput), "");
my $rbcOut = $runningBeanQuery->before();
foreach my $line (split /\n/, $rbcOut) {
# 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.
print "Line found \"$line\"\n" if $VERBOSE > 9;
unless ($seenSeperator) {
if ($line =~ /^\s*$/) {
$seenSeperator = 1;
} elsif ($currentFormat eq 'text' and $line =~ /^\s*\-+\s*$/) {
} elsif (($currentFormat eq 'text' and $line =~ /^\s*[\-\s]+\s*$/)
or ($currentFormat eq 'csv' and $line =~ /^\s*(\S+,)*\S+\s*$/)){
$seenSeperator = 1;
}
$prevLine = $line;