final versions of Route caching functions
[spider.git] / perl / Route / Node.pm
1 #
2 # Node routing routines
3 #
4 # Copyright (c) 2001 Dirk Koopman G1TLH
5 #
6 #
7 #
8
9 package Route::Node;
10
11 use DXDebug;
12 use Route;
13 use Route::User;
14 use DXUtil;
15 use DXJSON;
16 use Time::HiRes qw(gettimeofday);
17
18 use strict;
19
20 use vars qw(%list %valid @ISA $max $filterdef $obscount);
21 @ISA = qw(Route);
22
23 %valid = (
24                   nodes => '0,Nodes,parray',
25                   users => '0,Users,parray',
26                   usercount => '0,User Count',
27                   version => '0,Version',
28                   build => '0,Build',
29                   handle_xml => '0,Using XML,yesno',
30                   lastmsg => '0,Last Route Msg,atime',
31                   lastid => '0,Last Route MsgID',
32                   do_pc9x => '0,Uses pc9x,yesno',
33                   via_pc92 => '0,In via pc92?,yesno',
34                   obscount => '0,Obscount',
35                   last_PC92C => '9,Last PC92C',
36                   PC92C_dxchan => '9,PC92C hops,phash',
37 );
38
39 $filterdef = $Route::filterdef;
40 %list = ();
41 $max = 0;
42 $obscount = 3;
43 our $cachefn = localdata('route_node_cache');
44
45 sub count
46 {
47         my $n = scalar (keys %list);
48         $max = $n if $n > $max;
49         return $n;
50 }
51
52 sub max
53 {
54         count();
55         return $max;
56 }
57
58 #
59 # this routine handles the possible adding of an entry in the routing
60 # table. It will only add an entry if it is new. It may have all sorts of
61 # other side effects which may include fixing up other links.
62 #
63 # It will return a node object if (and only if) it is a completely new
64 # object with that callsign. The upper layers are expected to do something
65 # sensible with this!
66 #
67 # called as $parent->add(call, dxchan, version, flags)
68 #
69
70 sub add
71 {
72         my $parent = shift;
73         my $call = uc shift;
74         confess "Route::add trying to add $call to myself" if $call eq $parent->{call};
75         my $self = get($call);
76         if ($self) {
77                 $self->_addparent($parent);
78                 $parent->_addnode($self);
79                 return undef;
80         }
81         $self = $parent->new($call, @_);
82         $parent->_addnode($self);
83         dbg("CLUSTER: node $call added") if isdbg('cluster');
84         return $self;
85 }
86
87 #
88 # this routine is the opposite of 'add' above.
89 #
90 # It will return an object if (and only if) this 'del' will remove
91 # this object completely
92 #
93
94 sub del
95 {
96         my $self = shift;
97         my $pref = shift;
98
99         # delete parent from this call's parent list
100         $pref->_delnode($self);
101     $self->_delparent($pref);
102         my @nodes;
103         my $ncall = $self->{call};
104
105         # is this the last connection, I have no parents anymore?
106         unless (@{$self->{parent}}) {
107                 foreach my $rcall (@{$self->{nodes}}) {
108                         next if grep $rcall eq $_, @_;
109                         my $r = Route::Node::get($rcall);
110                         push @nodes, $r->del($self, $ncall, @_) if $r;
111                 }
112                 $self->_del_users;
113                 delete $list{$ncall};
114                 push @nodes, $self;
115                 dbg("CLUSTER: node $ncall deleted") if isdbg('cluster');
116         }
117         return @nodes;
118 }
119
120 # this deletes this node completely by grabbing the parents
121 # and deleting me from them, then deleting me from all the
122 # dependent nodes.
123 sub delete
124 {
125         my $self = shift;
126         my @out;
127         my $ncall = $self->{call};
128
129         # get rid of users and parents
130         $self->_del_users;
131         if (@{$self->{parent}}) {
132                 foreach my $call (@{$self->{parent}}) {
133                         my $parent = Route::Node::get($call);
134                         push @out, $parent->del($self) if $parent;
135                 }
136         }
137         # get rid of my nodes
138         push @out, $self->del_nodes;
139         # this only happens if we a orphan with no parents
140         if ($list{$ncall}) {
141                 push @out, $self;
142                 delete $list{$ncall};
143         }
144         return @out;
145 }
146
147 sub del_nodes
148 {
149         my $parent = shift;
150         my @out;
151         foreach my $rcall (@{$parent->{nodes}}) {
152                 my $r = get($rcall);
153                 push @out, $r->del($parent, $parent->{call}, @_) if $r;
154         }
155         return @out;
156 }
157
158 sub _del_users
159 {
160         my $self = shift;
161         for (@{$self->{users}}) {
162                 my $ref = Route::User::get($_);
163                 $ref->del($self) if $ref;
164         }
165         $self->{users} = [];
166 }
167
168 # add a user to this node
169 sub add_user
170 {
171         my $self = shift;
172         my $ucall = shift;
173         my $here = shift;
174         my $ip = shift;
175
176         confess "Trying to add NULL User call to routing tables" unless $ucall;
177
178         my $uref = Route::User::get($ucall);
179         my @out;
180         if ($uref) {
181                 @out = $uref->addparent($self);
182         } else {
183                 $uref = Route::User->new($ucall, $self->{call}, $here, $ip);
184                 @out = $uref;
185         }
186         $self->_adduser($uref);
187         $self->{usercount} = scalar @{$self->{users}};
188
189         return @out;
190 }
191
192 # delete a user from this node
193 sub del_user
194 {
195         my $self = shift;
196         my $ref = shift;
197         my @out;
198
199         if ($ref) {
200                 @out = $self->_deluser($ref);
201                 $ref->del($self);
202         } else {
203                 confess "tried to delete non-existant $ref->{call} from $self->{call}";
204         }
205         $self->{usercount} = scalar @{$self->{users}};
206         return @out;
207 }
208
209 # is a user on this node
210 sub is_user
211 {
212         my $self = shift;
213         my $call = shift;
214         return scalar grep {$_ eq $call} @{$self->{users}};
215 }
216
217 sub usercount
218 {
219         my $self = shift;
220         if (@_ && @{$self->{users}} == 0) {
221                 $self->{usercount} = shift;
222         }
223         return $self->{usercount};
224 }
225
226 sub users
227 {
228         my $self = shift;
229         return @{$self->{users}};
230 }
231
232 sub nodes
233 {
234         my $self = shift;
235         return @{$self->{nodes}};
236 }
237
238 sub rnodes
239 {
240         my $self = shift;
241         my @out;
242         foreach my $call (@{$self->{nodes}}) {
243                 next if grep $call eq $_, @_;
244                 push @out, $call;
245                 my $r = get($call);
246                 push @out, $r->rnodes($call, @_) if $r;
247         }
248         return @out;
249 }
250
251 # this takes in a list of node and user calls (not references) from
252 # a config type update for a node and returns
253 # the differences as lists of things that have gone away
254 # and things that have been added.
255 sub calc_config_changes
256 {
257         my $self = shift;
258         my %nodes = map {$_ => 1} @{$self->{nodes}};
259         my %users = map {$_ => 1} @{$self->{users}};
260         my $cnodes = shift;
261         my $cusers = shift;
262         if (isdbg('route')) {
263                 dbg("ROUTE: start calc_config_changes");
264                 dbg("ROUTE: incoming nodes on $self->{call}: " . join(',', sort @$cnodes));
265                 dbg("ROUTE: incoming users on $self->{call}: " . join(',', sort @$cusers));
266                 dbg("ROUTE: existing nodes on $self->{call}: " . join(',', sort keys %nodes));
267                 dbg("ROUTE: existing users on $self->{call}: " . join(',', sort keys %users));
268         }
269         my (@dnodes, @dusers, @nnodes, @nusers);
270         push @nnodes, map {my @r = $nodes{$_} ? () : $_; delete $nodes{$_}; @r} @$cnodes;
271         push @dnodes, keys %nodes;
272         push @nusers, map {my @r = $users{$_} ? () : $_; delete $users{$_}; @r} @$cusers;
273         push @dusers, keys %users;
274         if (isdbg('route')) {
275                 dbg("ROUTE: deleted nodes on $self->{call}: " . join(',', sort @dnodes));
276                 dbg("ROUTE: deleted users on $self->{call}: " . join(',', sort @dusers));
277                 dbg("ROUTE: added nodes on $self->{call}: " . join(',', sort  @nnodes));
278                 dbg("ROUTE: added users on $self->{call}: " . join(',', sort @nusers));
279                 dbg("ROUTE: end calc_config_changes");
280         }
281         return (\@dnodes, \@dusers, \@nnodes, \@nusers);
282 }
283
284
285 sub new
286 {
287         my $pkg = shift;
288         my $call = uc shift;
289
290         confess "already have $call in $pkg" if $list{$call};
291
292         my $self = $pkg->SUPER::new($call);
293         $self->{parent} = ref $pkg ? [ $pkg->{call} ] : [ ];
294         $self->{version} = shift || 5401;
295         $self->{flags} = shift || Route::here(1);
296         $self->{users} = [];
297         $self->{nodes} = [];
298         $self->{PC92C_dxchan} = {};
299         my $ip = shift;
300         $self->{ip} = $ip if defined $ip;
301         $self->reset_obs;                       # by definition
302
303         $list{$call} = $self;
304
305         return $self;
306 }
307
308 sub get
309 {
310         my $call = shift;
311         $call = shift if ref $call;
312         my $ref = $list{uc $call};
313         dbg("ROUTE: Failed to get Node $call" ) if !$ref && isdbg('routerr');
314         return $ref;
315 }
316
317 sub get_all
318 {
319         return values %list;
320 }
321
322 sub _addparent
323 {
324         my $self = shift;
325     return $self->_addlist('parent', @_);
326 }
327
328 sub _delparent
329 {
330         my $self = shift;
331     return $self->_dellist('parent', @_);
332 }
333
334
335 sub _addnode
336 {
337         my $self = shift;
338     return $self->_addlist('nodes', @_);
339 }
340
341 sub _delnode
342 {
343         my $self = shift;
344     return $self->_dellist('nodes', @_);
345 }
346
347
348 sub _adduser
349 {
350         my $self = shift;
351     return $self->_addlist('users', @_);
352 }
353
354 sub _deluser
355 {
356         my $self = shift;
357     return $self->_dellist('users', @_);
358 }
359
360 sub dec_obs
361 {
362         my $self = shift;
363         $self->{obscount}--;
364         return $self->{obscount};
365 }
366
367 sub reset_obs
368 {
369         my $self = shift;
370         $self->{obscount} = $obscount;
371 }
372
373 sub measure_pc9x_t
374 {
375         my $parent = shift;
376         my $t = shift;
377         my $lastid = $parent->{lastid};
378         if ($lastid) {
379                 return ($t < $lastid) ? $t+86400-$lastid : $t - $lastid;
380         } else {
381                 return 86400;
382         }
383 }
384
385 sub PC92C_dxchan
386 {
387         my $parent = shift;
388         my $call = shift;
389         my $hops = shift;
390         if ($call && $hops) {
391                 $hops =~ s/^H//;
392                 $parent->{PC92C_dxchan}->{$call} = $hops;
393                 return;
394         }
395         return (%{$parent->{PC92C_dxchan}});
396 }
397
398 sub TO_JSON { return { %{ shift() } }; }
399
400 sub write_cache
401 {
402         my $json = DXJSON->new;
403         $json->canonical(isdbg('routecache'));
404
405         my $ta = [ gettimeofday ];
406         my @s;
407         eval {
408                 while (my ($k, $v) = each  %list) {
409                     push @s, "$k:" . $json->encode($v) . "\n";
410             }
411         };
412         if (!$@ && @s) {
413                 my $fh = IO::File->new(">$cachefn") or confess("writing $cachefn $!");
414                 if (isdbg("routecache")) {
415                         $fh->print(sort @s);
416                 }
417                 else {
418                         $fh->print(@s);
419                 }
420                 $fh->close;
421         } else {
422                 dbg("Route::Node:Write_cache error '$@'");
423                 return;
424         }
425         $json->indent(0)->canonical(0);
426         my $diff = _diffms($ta);
427         dbg("Route::Node:WRITE_CACHE time to write: $diff mS");
428 }
429
430
431 sub DESTROY
432 {
433         my $self = shift;
434         my $pkg = ref $self;
435         my $call = $self->{call} || "Unknown";
436
437         dbg("ROUTE: destroying $pkg with $call") if isdbg('routelow');
438 }
439
440 #
441 # generic AUTOLOAD for accessors
442 #
443
444 sub AUTOLOAD
445 {
446         no strict;
447         my $name = $AUTOLOAD;
448         return if $name =~ /::DESTROY$/;
449         $name =~ s/^.*:://o;
450
451         confess "Non-existant field '$AUTOLOAD'" unless $valid{$name} || $Route::valid{$name};
452
453         # this clever line of code creates a subroutine which takes over from autoload
454         # from OO Perl - Conway
455         *$AUTOLOAD = sub {$_[0]->{$name} = $_[1] if @_ > 1; return $_[0]->{$name}};
456         goto &$AUTOLOAD;
457 }
458
459 1;
460