/
ayudante-lobo
executable file
·248 lines (236 loc) · 12.5 KB
/
ayudante-lobo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/env perl
package App::AyudanteLobo;
use strict;
#TODO POD documentation
use Cwd; use POSIX;
use Date::Format qw/time2str/;
use Net::SSH2; use Try::Tiny;
use POE; use POE::Component::DirWatch::WithCaller;
use Unix::PID; use YAML;
#Load platform-specific modules here too to avoid missing modules causing a crash later
for ($^O) {
#Clipboard.pm doesn't integrate nicely with the OSX clipboard due to datatype differences
#nor does it integrate well with windows, as it doesn't support empty()
require Mac::Pasteboard if /darwin/;
require Win32::Clipboard if /MSWin32/;
#Unsurprisingly it's perfectly fine for linux :/
require Clipboard unless /darwin/ or /MSWin32/;
#Speech::Synthesis is a great wrapper for Win32::SAPI5 and should work for Festival
#It unfortunately doesn't work -at all- on modern OSX due to API changes/Carbon deprecation
require Speech::Synthesis unless /darwin/;
#Win32::API needed here for moderately pleasant use of SnoreNotify
require Win32::API if /MSWin32/;
}
my $ME = __PACKAGE__;
#will bump to v1.0 upon full cross-platform compatibility and submission to CPAN.
#version number reflective of how close the module is to 'complete'
my $VERSION = "0.7";
my $HOSTNAME = `hostname`; chomp $HOSTNAME;
my ($base, $Conf);
my $running = 0;
my $sighup = 0;
sub scp_upload {
my $file = shift; my $ssh = Net::SSH2->new();
try {
$ssh->connect($Conf->{'upload'}->{server},$Conf->{'upload'}->{port},Timeout => 3);
} catch {
logger(2,"SSH connection failed (exception): $_") and return 0;
};
logger(2,"SSH connection failed (error): ".$ssh->error()) and return 0 if $ssh->error;
$ssh->auth_publickey($Conf->{'upload'}->{user},$Conf->{'upload'}->{sshkeypath}.'.pub',$Conf->{'upload'}->{sshkeypath});
logger(2,"SSH connection failed (auth error): ".$ssh->error()) and return 0 unless $ssh->auth_ok;
$Conf->{'upload'}->{remotepath} .= "/" unless $Conf->{'upload'}->{remotepath} =~ /\/$/;
logger(2,"File upload failed: ".$ssh->error) and return 0 unless $ssh->scp_put("$file",$Conf->{'upload'}->{remotepath}.$file->basename);
$ssh->disconnect; return 1;
}
sub timefmt2str {
return Date::Format::time2str(shift,time());
}
sub clipb_copy {
my $t = shift;
Clipboard->copy($t) and return unless $^O eq 'darwin' or $^O eq 'MSWin32';
Win32::Clipboard::Empty() and Win32::Clipboard::Set($t) and return unless $^O eq 'darwin';
my $p = Mac::Pasteboard->new();
$p->clear(); #Clear clipboard first to avoid a clipboard ownership error
$p->copy($t);
$p->copy($t, "public.utf8-plain-text");
$p->copy($t, "public.utf16-plain-text");
$p->copy($t, "public.utf16-external-plain-text");
}
sub banner {
#TODO make these less ghetto.
#There might be something in Win32::OLE I can use for toast notifications.
#Mac::Carbon probably provides something too, but it's too old to use on modern OSX.
my ($title,$text) = @_;
system($Conf->{general}->{'notify'}->{toast_exe}." -appID MaffC.App-AyudanteLobo -silent -t \"$title\" -m \"$text\"") if $^O eq 'MSWin32';
system("/usr/bin/osascript -e 'display notification \"$text\" with title \"$title\"' &") if $^O eq 'darwin';
#TODO some form of notification banner on *nix
}
sub speak_osx {
my ($r,$v);
my $t=shift;
#We have to use `say` here because Mac::Speech is old and busted.
$v = "-v".$Conf->{general}->{'notify'}->{speech_voice} if length $Conf->{general}->{'notify'}->{speech_voice};
$r = "-r".$Conf->{general}->{'notify'}->{speech_rate} if length $Conf->{general}->{'notify'}->{speech_rate};
system("/usr/bin/say $v $r '$t' &");
}
sub speak_w32 {
#TODO genericise this sub, set engine to sapi5/festival based on platform
my $t=shift;
my %args=(
engine => 'SAPI5',
voice => ''
);
$args{voice}=$Conf->{general}->{'notify'}->{speech_voice} if length $Conf->{general}->{'notify'}->{speech_voice};
my $synth=Speech::Synthesis->new(%args);
$synth->speak($t);
}
sub speak {
for ($^O) {
speak_osx(@_) if /darwin/;
speak_w32(@_) if /MSWin32/;
}
#TODO festival support on *nix
}
# Functions
sub init {
#if $HOME ('nix) or $HOMEDRIVE/$HOMEPATH (win32) exist, use those for our basedir, else use cwd
$base = $ENV{HOME};
if($^O eq 'MSWin32') {
$base = $ENV{HOMEDRIVE}.$ENV{HOMEPATH};
$base =~ s/\\/\//g;
}
$base = cwd() unless length $base and -d $base;
#chdir to ensure relative paths work as expected for the user
chdir $base;
my $confp = $ENV{LOBORC} || "$base/.ayudante-loborc"; my $loaded = 0;
$loaded = init_conf($confp) if -e $confp and -f $confp and -r $confp and not -z $confp;
if ($Conf->{general}->{storelogs} or not $loaded) {
open(STDOUT, ">>".$Conf->{general}->{logfile}) if length $Conf->{general}->{logfile};
open(STDERR, ">>".$Conf->{general}->{errlogfile}) if length $Conf->{general}->{errlogfile};
select((select(STDOUT), $|=1)[0]);
}
$loaded == 0 and logger(9,"Configuration file at $confp either doesn't exist or is unreadable/empty.");
$loaded == -1 and logger(9,"Configuration file at $confp exists but could not be loaded. Please check it is fully-valid YAML.");
$loaded == -2 and logger(9,"Configuration file at $confp loaded but did not contain any enabled monitors.");
$loaded == -3 and logger(9,"Configuration file at $confp loaded but was missing a required configuration parameter.");
#then we run the kernel once, I forget why but I remember it being a problem
POE::Kernel->run();
#at this point the config file should be /pretty/ kawaii, so we start initialising
my $pid = Unix::PID->new()->is_pidfile_running($Conf->{general}->{pidfile}) || 0;
kill 'HUP', $pid and logger(8, "$ME already running, restarting.") if $pid != $$ and $pid > 0;
Unix::PID->new()->pid_file($Conf->{general}->{pidfile}) or logger(9, "Failed to write PID to ".$Conf->{general}->{pidfile});
#indicate we should start
logger(1,"Starting $ME..");
$running = 1;
#set up AppUserModelID for windows' toast notifications
if (defined $Conf->{general}->{'notify'}->{'banner'} and $Conf->{general}->{'notify'}->{'banner'} and length $Conf->{general}->{'notify'}->{toast_exe}) {
my $SetProcessAppID = Win32::API::More->new('shell32', 'SetCurrentProcessExplicitAppUserModelID', 'N', 'n');
logger(9,"Error while loading Shell32:SetCurrentProcessExplicitAppUserModelID via Win32::API: $^E") unless $SetProcessAppID;
#$SetProcessAppID->UseMI64(1); #Commented out because perl complains that no such subroutine exists
my $ret = $SetProcessAppID->Call(unpack('J',pack('p',"MaffC.App-AyudanteLobo")));
logger(9,"Error while calling Shell32:SetCurrentProcessExplicitAppUserModelID: $^E") unless $ret == 0;
#Windows requires a shortcut exist in order for notifications from an AppID to actually succeed, so we create one where the user won't see it.
#This is done upon startup, with no care for if a shortcut already exists. It'll either fail to create, which is fine, or overwrite, which is also fine.
system($Conf->{general}->{'notify'}->{toast_exe}." -install \"Startup\\ayudante-lobo\" $ENV{PAR_PROGNAME} MaffC.App-AyudanteLobo");
}
#set up signal handlers so we can handle SIGHUPs and handle quitting gracefully.
$SIG{$_} = \&sigtrap for qw/HUP INT QUIT TERM/;
#then we initialise monitors
init_mons();
logger(1, "$ME version $VERSION started.");
}
sub init_conf {
$Conf = YAML::LoadFile(shift) or return -1;
$base = $Conf->{general}->{home} if defined $Conf->{general}->{home} and length $Conf->{general}->{home};
chdir $base;
return -3 unless defined $Conf->{general}->{tmp} and length $Conf->{general}->{tmp};
$Conf->{general}->{tmp} .= '/' unless $Conf->{general}->{tmp} =~ /\/$/;
mkdir $Conf->{general}->{tmp} or logger(9,"Error creating work directory ".$Conf->{general}->{tmp}.": $!") unless -d $Conf->{general}->{tmp};
$Conf->{general}->{storelogs} = 1 unless defined $Conf->{general}->{storelogs} and $Conf->{general}->{storelogs} =~ /^[01]$/;
$Conf->{general}->{pidfile} = "$base/.$ME.pid" unless exists $Conf->{general}->{pidfile};
$Conf->{general}->{logfile} = "$base/.$ME.log" unless exists $Conf->{general}->{logfile};
$Conf->{general}->{errlogfile} = "$base/.$ME.err" unless defined $Conf->{general}->{errlogfile} and length $Conf->{general}->{errlogfile};
$Conf->{general}->{storelogs} = 0 unless length $Conf->{general}->{logfile} or length $Conf->{general}->{errlogfile};
return -2 unless scalar keys %{$Conf->{monitor}};
my $c;
for my $monitor (keys %{$Conf->{monitor}}) { $c++ unless defined $Conf->{monitor}->{$monitor}->{disable} and $Conf->{monitor}->{$monitor}->{disable} == 1; }
return -2 unless $c;
}
sub init_mons {
POE::Session->create( inline_states => { _start => sub {
foreach our $monitor (keys %{$Conf->{monitor}}) {
next if defined $Conf->{monitor}->{$monitor}->{disable} and $Conf->{monitor}->{$monitor}->{disable} == 1;
$Conf->{monitor}->{$monitor}->{poll} = 5 unless defined $Conf->{monitor}->{$monitor}->{poll};
$Conf->{monitor}->{$monitor}->{ignoreseen} = 0 unless defined $Conf->{monitor}->{$monitor}->{ignoreseen};
$_[HEAP]->{$monitor} = POE::Component::DirWatch::WithCaller->new(
alias => $monitor,
directory => $Conf->{monitor}->{$monitor}->{dir},
filter => \&filter,
file_callback => \&trigger,
interval => $Conf->{monitor}->{$monitor}->{poll},
ignore_seen => $Conf->{monitor}->{$monitor}->{ignoreseen},
ensure_seen => $Conf->{monitor}->{$monitor}->{ignoreseen},
);
}
}});
}
sub sigtrap {
my $sig = shift;
logger(2, "Caught SIG$sig: ".($sig eq 'HUP'? 'Restarting..' : 'Exiting..'));
$running = 0;
$sig eq 'HUP' and $sighup = 1;
}
sub logger {
my ($pri,$msg) = @_;
print timefmt2str('%e %B %T')." $HOSTNAME $ME\[$$] ($pri): $msg\n" unless $pri =~ /^[29]$/;
print STDERR timefmt2str('%e %B %T')." $HOSTNAME $ME\[$$] ($pri): $msg\n" if $pri =~ /^[29]$/;
notify($ME,$msg) if $pri == 3 and $Conf->{general}->{'notify'}->{on}->{error} == 1;
exit 0 if $pri == 8;
exit 1 if $pri == 9;
return $pri;
}
# Monitor-specific subroutines.
sub filter {
my ($sender_mon,$file) = @_;
return 0 if $file->is_dir;
return $file =~ /$Conf->{monitor}->{$sender_mon}->{match}->{regexp}/ if defined $Conf->{monitor}->{$sender_mon}->{match}->{regexp};
return 1 if defined $Conf->{monitor}->{$sender_mon}->{match}->{spotlight_meta} and qx(/usr/bin/mdls -name $Conf->{monitor}->{$sender_mon}->{match}->{spotlight_meta} "$file") =~ /^$Conf->{monitor}->{$sender_mon}->{match}->{spotlight_meta} = (?!\(null\)).*$/;
return 0;
}
sub trigger {
my ($sender_mon,$file) = @_;
#logger(1,"sender: $sender_mon, file: $file");
$file->move_to($Conf->{general}->{tmp}.name($sender_mon,$file->basename));
upload($file) or $file->move_to($Conf->{general}->{tmp}.$file->basename) and return if defined $Conf->{monitor}->{$sender_mon}->{action}->{'upload'} and $Conf->{monitor}->{$sender_mon}->{action}->{'upload'} == 1;
$file->move_to($Conf->{monitor}->{$sender_mon}->{action}->{target}."/".$file->basename) or logger(2,"Couldn't move ".$file->basename." to ".$Conf->{monitor}->{$sender_mon}->{action}->{target}) if defined $Conf->{monitor}->{$sender_mon}->{action}->{move} and $Conf->{monitor}->{$sender_mon}->{action}->{move} == 1 and defined $Conf->{monitor}->{$sender_mon}->{action}->{target};
$file->remove() if defined $Conf->{monitor}->{$sender_mon}->{action}->{delete} and $Conf->{monitor}->{$sender_mon}->{action}->{delete} == 1;
}
sub notify {
banner(@_) if $Conf->{general}->{'notify'}->{'banner'} == 1;
speak($_[($_[0] eq $ME)? 1 : 0]) if $Conf->{general}->{'notify'}->{speech} == 1;
}
sub name {
my ($sender_mon,$filename) = @_;
$Conf->{monitor}->{$sender_mon}->{action}->{rename} = 1 unless defined $Conf->{monitor}->{$sender_mon}->{action}->{rename};
return $filename if $Conf->{monitor}->{$sender_mon}->{action}->{rename} =~ /^0$/;
my $genfilename = timefmt2str($Conf->{general}->{filename});
return $genfilename."_$filename" if $Conf->{monitor}->{$sender_mon}->{action}->{rename} eq 'prepend';
$filename =~ s/(\.[a-z0-9]+)$//i and $genfilename .= $1;
return $filename."_$genfilename" if $Conf->{monitor}->{$sender_mon}->{action}->{rename} eq 'append';
return $genfilename;
}
sub upload {
my $file = shift;
notify("Uploading file","Uploading ".$file->basename." to ".$Conf->{'upload'}->{server}.".") if $Conf->{general}->{'notify'}->{on}->{'upload'} == 1;
scp_upload($file) or logger(3,"Failed to upload ".$file->basename.".") and return 0;
clipb_copy("http".(($Conf->{'upload'}->{pubssl}==1)? 's' : '')."://".$Conf->{'upload'}->{pubdomain}."/".($file->basename =~ s/ /%20/r));
notify("File uploaded",$file->basename." uploaded to ".$Conf->{'upload'}->{server}.".") if $Conf->{general}->{'notify'}->{on}->{'upload'} == 1;
return 1;
}
# Main
init();
POE::Kernel->run_while(\$running);
logger($sighup? 1 : 8,"Halting $ME..");
#TODO investigate a way to signal lobo to restart on windows, since SIGHUP isn't supported
exec $^X, $0, @ARGV;