cmdline.pm 13.4 KB
Newer Older
1
# Commandline user interface
2
# Copyright (C) 2009-2014, AllWorldIT
Nigel Kukard's avatar
Nigel Kukard committed
3
# Copyright (C) 2008, LinuxRulz
Nigel Kukard's avatar
Nigel Kukard committed
4
# Copyright (C) 2005-2007 Nigel Kukard  <nkukard@lbsd.net>
5
#
6
7
8
9
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
10
#
11
12
13
14
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
15
#
16
17
18
19
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Nigel Kukard's avatar
Nigel Kukard committed
20
21
22



Nigel Kukard's avatar
Nigel Kukard committed
23
package wiaflos::client::cmdline;
Nigel Kukard's avatar
Nigel Kukard committed
24
25
26
27
28
29
30
31
32
33
34

use strict;

# Exporter stuff
require Exporter;
our (@ISA,@EXPORT);
@ISA = qw(Exporter);
@EXPORT = qw(
	execute
);

35
use POSIX qw( floor );
36
use Text::ParseWords;
Nigel Kukard's avatar
Nigel Kukard committed
37

38
use wiaflos::constants;
Nigel Kukard's avatar
Nigel Kukard committed
39
40
41
use wiaflos::client::config;
use wiaflos::client::soap;
use wiaflos::client::misc;
42

Nigel Kukard's avatar
Nigel Kukard committed
43
44


Nigel Kukard's avatar
Nigel Kukard committed
45
46
47
48
49
50
51
52
53
54
55
# Modules to load
my @modules = qw(
	Engine
	GL
	Tax
	Inventory
	Clients
	Invoicing
	Suppliers
	Purchasing
	Payments
Nigel Kukard's avatar
Nigel Kukard committed
56
	Receipting
57
	Statements
58
	SupplierCreditNotes
Nigel Kukard's avatar
Nigel Kukard committed
59
	SupplierReceipting
Nigel Kukard's avatar
Nigel Kukard committed
60
	Reporting
Nigel Kukard's avatar
Nigel Kukard committed
61
	YearEnd
Nigel Kukard's avatar
Nigel Kukard committed
62
63
);
my @plugins;
Nigel Kukard's avatar
Nigel Kukard committed
64
my $cmdline;
Nigel Kukard's avatar
Nigel Kukard committed
65

Nigel Kukard's avatar
Nigel Kukard committed
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# Merge menu items
sub mergeMenu
{
	my ($menuA,$menuB) = @_;


	# Loop with menu B
	foreach my $itemB (@{$menuB}) {
		my $node;

#		print(STDERR "ItemB: ".$itemB->{'MenuItem'}."\n");

		# Loop with main menu we merging into to find the sub item
		foreach my $itemA (@{$menuA->{'Children'}}) {
#			print(STDERR "ItemA: ".$itemA->{'MenuItem'}.", ItemB: ".$itemB->{'MenuItem'}."\n");

			# Check for match
			if ($itemB->{'MenuItem'} eq $itemA->{'MenuItem'}) {
#				print(STDERR "Match\n");
				$node = $itemA;
			}
		}
Nigel Kukard's avatar
Nigel Kukard committed
88

Nigel Kukard's avatar
Nigel Kukard committed
89
90
91
92
93
94
95
96
97
98
		# Check for match
		if (!defined($node)) {
#			print(STDERR "No match, adding\n");
			push(@{$menuA->{'Children'}},$itemB);
		} else {
			# Merge children
			mergeMenu($node,$itemB->{'Children'});
		}
	}
}
Nigel Kukard's avatar
Nigel Kukard committed
99

100
101

# Start our interface
Nigel Kukard's avatar
Nigel Kukard committed
102
my $cmdMap;
103
my @cmdHistory = ();
Nigel Kukard's avatar
Nigel Kukard committed
104
my @cmdPos = ();
Nigel Kukard's avatar
Nigel Kukard committed
105
106
sub ui_start
{
Nigel Kukard's avatar
Nigel Kukard committed
107
108
109
110
111
112
113
	# Load modules
	foreach my $module (@modules) {
		loadPlugin($module);
	}

	# Build command map
	$cmdMap->{'Children'} = [
114
		# Core
Nigel Kukard's avatar
Nigel Kukard committed
115
116
117
118
119
120
121
122
123
124
125
126
		{
			'MenuItem'	=> 'Quit',
			'Regex'		=> 'quit$',
			'Desc'		=> 'Exit',
			'Help'		=> 'quit',
			'Function'	=> \&cmd_quit,
		},
		{
			'MenuItem'	=> 'Connect',
			'Regex'		=> 'connect',
			'Desc'		=> 'Connect to an accounting server',
			'Help'		=> 'connect <username> <password> [uri]',
127
			'Function'	=> \&cmd_connect,
Nigel Kukard's avatar
Nigel Kukard committed
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
		},
		{
			'MenuItem'	=> 'Load',
			'Regex'		=> 'load',
			'Desc'		=> 'Load a wiaflos file',
			'Help'		=> 'load file="<filename>"',
			'Function'	=> \&loadFile,
		},
	];

	# Merge menu's
	foreach my $plugin (@plugins) {
		mergeMenu($cmdMap,$plugin->{'Menu'});
	}

143
144
	# Setup term
	my $OUT = \*STDOUT;
Nigel Kukard's avatar
Nigel Kukard committed
145

146
147
148
149
150
151
	# Check for command-line stuff
	if ($cmdline->{'connect'}) {
		if (processCommand($OUT,"connect $cmdline->{'connect'}")) {
			exit 1;
		}
	}
Nigel Kukard's avatar
Nigel Kukard committed
152

153
	# Check if we using raw stdin
Nigel Kukard's avatar
Nigel Kukard committed
154
155
156
157
158
159
160
161
162
163
164
	if (defined(my $file = $cmdline->{'load-file'})) {
		# - is a special filename used for STDIN
		if ($file eq "-") {
			# Read from stdin - hack as we don't have a function
			while (my $line = <STDIN>) {
				# Chomp off newline
				chomp($line);
				# Process command
				if (my $res = processCommand($OUT,$line) < 0) {
					exit 1;
				}
165
			}
Nigel Kukard's avatar
Nigel Kukard committed
166
167
		# For everything else try open the file
		} else {
168
169
			# We don't need line buffering when processing files...
			$OUT->autoflush();
Nigel Kukard's avatar
Nigel Kukard committed
170
			# Exit with code given by the loadFile function
Nigel Kukard's avatar
Nigel Kukard committed
171
			if (loadFile($OUT,"file='$file'") < 0) {
Nigel Kukard's avatar
Nigel Kukard committed
172
173
				exit 1;
			}
174
175
176
177
		}
		# Exit normally
		exit 0;
	}
Nigel Kukard's avatar
Nigel Kukard committed
178

179
180
	# Start interactive interface
	use Term::ReadLine;
Nigel Kukard's avatar
Nigel Kukard committed
181
	my $term = new Term::ReadLine 'Wiaflos Accounting';
Nigel Kukard's avatar
Nigel Kukard committed
182

183
	$OUT = $term->OUT;
184

Nigel Kukard's avatar
Nigel Kukard committed
185
186
	# While input process, else its ctrl-D
	while (defined(my $cmdline = $term->readline(join('/',@cmdPos)."> "))) {
187
188
189
		processCommand($OUT,$cmdline);
		$term->addhistory($cmdline);
	}
Nigel Kukard's avatar
Nigel Kukard committed
190
191
192

	print($OUT "\n");
	cmd_quit($OUT);
193
}
Nigel Kukard's avatar
Nigel Kukard committed
194
195


Nigel Kukard's avatar
Nigel Kukard committed
196
# Load an wiaflos file
197
198
199
sub loadFile
{
	my ($OUT,@args) = @_;
200

201
	my $parms = parseArgs(@args);
Nigel Kukard's avatar
Nigel Kukard committed
202
203


204
205
206
	if (!defined($parms->{'file'})) {
		return -202;
	}
Nigel Kukard's avatar
Nigel Kukard committed
207

208
	# Open file
209
210
	my $FH;
	if (!open($FH,"<",$parms->{'file'})) {
211
212
213
		print($OUT " => Failed to open '".$parms->{'file'}."': $!\n");
		return -1;
	}
Nigel Kukard's avatar
Nigel Kukard committed
214

215
	# Process lines
216
	printf($OUT " => Loading file '%s'\n",$parms->{'file'});
217
218
219

	# Grab number of lines to process
	my $line_total = 0;
220
	while (<$FH>) { $line_total++ }
221
222

	# Reset back to beginning of file and re-read
223
	seek($FH,0,0);
Nigel Kukard's avatar
Nigel Kukard committed
224
225
226
227
	# Reset line counter (NK: UNDOCUMENTED)
	$FH->input_line_number(0);

	# Loop again with each line number
228
229
	my $line_current = 0;
	my $percent_last = "";
230
	my @history = ();
231
	while (my $cmdline = <$FH>) {
232
233
234
235
		# Bump line number
		$line_current++;

		# Grab percent done
236
		my $percent_done = sprintf('%5.1f',floor(($line_current / $line_total) * 1000) / 10);
237
		# If its changed, output and save
238
239
		if ($percent_done ne $percent_last) {
				print($OUT "    Progress: $percent_done\n");
240
241
242
243
				$percent_last = $percent_done;
		}

		# Nuke whitespaces at end
244
		chomp($cmdline);
245
246
247
248

		# Short circuit blank lines
		next if ($cmdline eq "");

249
250
		# Process
		if ((my $res = processCommand($OUT,$cmdline)) < 0) {
251
252
253
254
255
256
			# List last 5 success lines
			foreach my $line (@history) {
				print($OUT " => SUCCESS: $line\n");
			}
			# Lastly list the failure
			print($OUT " => FAILURE: $cmdline\n");
257
258
			return $res;
		}
259
260
261
262
263
264
265

		# Store history so we can give better errors
		push(@history,$cmdline);
		# Store only 5 lines of history
		if (@history > 5) {
			shift(@history);
		}
266
	}
Nigel Kukard's avatar
Nigel Kukard committed
267

268
	# Close
269
	close($FH);
Nigel Kukard's avatar
Nigel Kukard committed
270
271


272
273
274
275
	return 0;
}


276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# Command: connect
sub cmd_connect
{
	my ($OUT,@args) = @_;


	# Special case for local
	if (@args > 0 && $args[0] eq "local") {
		my $cfg;

		# Set defaults
		$cfg->{'config_file'} = CONFIG_FILE;

		# Parse commandline arguments
		if (defined($cmdline->{'config'}) && $cmdline->{'config'} ne "") {
			$cfg->{'config_file'} = $cmdline->{'config'};
		}

		# Check config file exists
		if (! -f $cfg->{'config_file'}) {
			print(STDERR "ERROR: No configuration file '".$cfg->{'config_file'}."' found!\n");
			exit 1;
		}

		# Load server modules
		eval(qq(
			use Config::IniFiles;
			use awitpt::cache;
			use wiaflos::server::api::auth;
			use wiaflos::server::core::config;
			use wiaflos::server::core::jobs;
			use wiaflos::server::core::templating;
		));
		if ($@) {
			print(STDERR "Error loading server core ($@)\n");
			exit 1;
		}

		# Use config file, ignore case
		tie my %inifile, 'Config::IniFiles', (
				-file => $cfg->{'config_file'},
				-nocase => 1
		) or die "Failed to open config file '".$cfg->{'config_file'}."': $!";
		# Copy config
		my %config = %inifile;

		my $server = wiaflos::client::fakeserver->new();
		$server->{'inifile'} = \%config;

		# Init authentication
		wiaflos::server::api::auth::init($server);
		# Init config
		wiaflos::server::core::config::Init($server);
		# Init templating engine
		wiaflos::server::core::templating::Init($server);
		# Init caching engine
332
333
		# NK: As we're local disable caching
		$awitpt::cache::cache_type = "none";
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
		awitpt::cache::Init($server);
		# Init jobs
		wiaflos::server::core::jobs::Init($server);

		# Setup database handle
		awitpt::db::dblayer::setHandle($server->{'client'}->{'dbh'});

		eval(qq(
			use wiaflos::server::api::SOAPTest;
			use wiaflos::server::api::Engine;
			use wiaflos::server::api::GL;
			use wiaflos::server::api::Clients;
			use wiaflos::server::api::Inventory;
			use wiaflos::server::api::Invoicing;
			use wiaflos::server::api::Payments;
			use wiaflos::server::api::Purchasing;
			use wiaflos::server::api::Receipting;
			use wiaflos::server::api::Reporting;
			use wiaflos::server::api::Statements;
			use wiaflos::server::api::SupplierCreditNotes;
			use wiaflos::server::api::SupplierReceipting;
			use wiaflos::server::api::Suppliers;
			use wiaflos::server::api::Tax;
			use wiaflos::server::api::YearEnd;
		));
		if ($@) {
			print(STDERR "Error loading API ($@)\n");
			exit 1;
		}
	}

	return soapConnect($OUT,@args);
}


369
370
371
372
# Command: quit
sub cmd_quit
{
	my ($OUT,@args) = @_;
373

374
	print($OUT "Exiting...\n");
Nigel Kukard's avatar
Nigel Kukard committed
375

376
377
	exit 0;
}
Nigel Kukard's avatar
Nigel Kukard committed
378

379
380
381
382
383
384
385
386
387
388

# Process command
sub processCommand
{
	my ($OUT,$cmdline) = @_;


	# If whitespace only or comment
	return 0 if ($cmdline =~ /^\s*$/ || $cmdline =~ /^\s*#/);

389
390
391
392
393
	# Optimize by removing spaces from beginning and end of line
	$cmdline =~ s/^\s+//;
	chomp($cmdline);


394
	# Check for special: ..
395
	if ($cmdline eq "..") {
396
397
398
		# If we have history, shift it in
		if (@cmdHistory > 0) {
			$cmdMap = pop(@cmdHistory);
Nigel Kukard's avatar
Nigel Kukard committed
399
			pop(@cmdPos);
400
401
		} else { # Else error to client
			print($OUT " => You are already on the top level.\n");
Nigel Kukard's avatar
Nigel Kukard committed
402
403
		}

404
		return 0;
Nigel Kukard's avatar
Nigel Kukard committed
405
406


407
	# Check for special: help
408
	} elsif ($cmdline eq "help") {
409
		print($OUT "Valid commands...\n");
Nigel Kukard's avatar
Nigel Kukard committed
410

411
		# Loop commands
Nigel Kukard's avatar
Nigel Kukard committed
412
		foreach my $i (@{$cmdMap->{'Children'}}) {
413
			# Check if valid in this state
414
#			if (!defined($i->{'type'})
Nigel Kukard's avatar
Nigel Kukard committed
415
#					|| (
416
417
418
#						defined($i->{'type'}) && ($i->{'type'} == 0
#						|| (!soapConnected() && $i->{'type'} == 1)
#						|| (soapConnected() && $i->{'type'} == 2)))
Nigel Kukard's avatar
Nigel Kukard committed
419
420
421
422
423
424
425
426
#					) {
#				print($OUT $i->{'MenuItem'}."\t- ".$i->{'Desc'}."\n");
#			}
			# Check type of menu item
			if (defined($i->{'Children'})) {
				print($OUT $i->{'MenuItem'}."/\n");
			} else {
				print($OUT $i->{'MenuItem'}."\n");
427
428
			}
		}
Nigel Kukard's avatar
Nigel Kukard committed
429

430
		return 0;
Nigel Kukard's avatar
Nigel Kukard committed
431

432
	# Check for special: help <command>
433
	} elsif (my ($cmd) = ($cmdline =~ /^help\s*(\S+)$/i)) {
434
435
		my $res = 0;
		# Loop with commands
Nigel Kukard's avatar
Nigel Kukard committed
436
		foreach my $i (@{$cmdMap->{'Children'}}) {
437
438
			# Hop over children menu items
			next if (defined($i->{'Children'}));
439
#		print(STDERR "DEBUG: cmdline = '$cmd' vs. '$i->{'Regex'}'\n");
440
441

			if ($cmd =~ /^\s*$i->{'Regex'}\s*$/i) {
442
				print($OUT "Usage...\n");
Nigel Kukard's avatar
Nigel Kukard committed
443
				print($OUT "  ".$i->{'Help'}."\n");
444
				$res = 1;
Nigel Kukard's avatar
Nigel Kukard committed
445
				last;
Nigel Kukard's avatar
Nigel Kukard committed
446
447
448
			}
		}

449
450
451
		print($OUT " => ERROR - Command not found\n") if (!$res);

		return 0;
Nigel Kukard's avatar
Nigel Kukard committed
452
453
454
455
	}



456
	my $res = -200;
457
	my $matchedCmd;
Nigel Kukard's avatar
Nigel Kukard committed
458

459
	# Loop with commands & test
Nigel Kukard's avatar
Nigel Kukard committed
460
	foreach my $i (@{$cmdMap->{'Children'}}) {
Nigel Kukard's avatar
Nigel Kukard committed
461

462
		# Test command for match
463
		if ((defined($i->{'Children'}) && $cmdline =~ /^\s*$i->{'MenuItem'}\s*$/i)
464
				|| (defined($i->{'Regex'}) && $cmdline =~ /^\s*$i->{'Regex'}(?:\s+|$)/i) ) {
465
466

			# Check state definition
467
#			if (!defined($i->{'type'})
Nigel Kukard's avatar
Nigel Kukard committed
468
#					|| (
469
470
471
#						defined($i->{'type'}) && ($i->{'type'} == 0
#						|| (!soapConnected() && $i->{'type'} == 1)
#						|| (soapConnected() && $i->{'type'} == 2)))
Nigel Kukard's avatar
Nigel Kukard committed
472
#					) {
473
474

				# Check if we have children commands
Nigel Kukard's avatar
Nigel Kukard committed
475
				if (defined($i->{'Children'})) {
476
477
					# Save history
					push(@cmdHistory,$cmdMap);
Nigel Kukard's avatar
Nigel Kukard committed
478
					push(@cmdPos,$i->{'MenuItem'});
479
					# Change map
Nigel Kukard's avatar
Nigel Kukard committed
480
					$cmdMap = $i;
481
482
483
484
					# Success
					$res = undef;
				} else {
					# Substitute
Nigel Kukard's avatar
Nigel Kukard committed
485
					my $substitution = $i->{'Substitute'} ? defined($i->{'Substitute'}) : "";
486
					(my $fcmd = $cmdline) =~ s/^\s*$i->{'Regex'}\s*/$substitution/i;
487

488
#					print($OUT "Found '".$i->{'Regex'}."', dest '".$fcmd."'\n");
489

490
					# Pull args
491
492
493
					my @args = shellwords($fcmd);

					# If defined, call ... else exit
Nigel Kukard's avatar
Nigel Kukard committed
494
					$res = $i->{'Function'}($OUT,@args);
495
496

					$matchedCmd = $i;
497
				}
Nigel Kukard's avatar
Nigel Kukard committed
498
499
500
#			} else {
#				$res = -201;
#			}
501

502
503
504
			last;
		}
	}
Nigel Kukard's avatar
Nigel Kukard committed
505

506

507
508
509
510
	# If no command, give error
	if (!defined($res)) {
		return 0;
	} elsif ($res == 0) {
511
#		print($OUT " => Ok!\n");
512
	} elsif	($res > 0) {
513
#		print($OUT " => Ok! (res: $res)\n");
514
	} elsif ($res == -103) {
Nigel Kukard's avatar
Nigel Kukard committed
515
		print($OUT " => ERROR - Not authorized to access this function: $cmdline\n");
516
	} elsif ($res == -200) {
Nigel Kukard's avatar
Nigel Kukard committed
517
		print($OUT " => ERROR - Invalid command: $cmdline\n");
518
	} elsif ($res == -201) {
Nigel Kukard's avatar
Nigel Kukard committed
519
		print($OUT " => ERROR - Command not valid in this state: $cmdline\n");
520
	} elsif ($res == -202) {
Nigel Kukard's avatar
Nigel Kukard committed
521
		print($OUT " => ERROR - Usage error: $cmdline\n");
Nigel Kukard's avatar
Nigel Kukard committed
522
		print($OUT "  ".$matchedCmd->{'Help'}."\n");
523
	} else {
524
		print($OUT " => ERROR - Processing line: $cmdline\n");
525
526
		print($OUT " => ERROR - Unknown result: $res\n");
	}
Nigel Kukard's avatar
Nigel Kukard committed
527

528
529
	return $res;
}
Nigel Kukard's avatar
Nigel Kukard committed
530

531
532
533
534


sub execute
{
Nigel Kukard's avatar
Nigel Kukard committed
535
536
	$cmdline = shift;

Nigel Kukard's avatar
Nigel Kukard committed
537
538
539
540
541
	ui_start;
}



Nigel Kukard's avatar
Nigel Kukard committed
542
543
544
545
546
547
548
549
550
551
552
553
554






# Load a plugin
sub loadPlugin
{
	my $plugin = shift;


	# Load module
555
	my $res = eval(qq(
Nigel Kukard's avatar
Nigel Kukard committed
556
557
		use wiaflos::client::cmdline::$plugin;
		registerPlugin(\"$plugin\",\$wiaflos::client::cmdline::${plugin}::pluginInfo);
558
	));
Nigel Kukard's avatar
Nigel Kukard committed
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
	if ($@ || (defined($res) && $res != 0)) {
		print(STDERR "Error loading plugin $plugin ($res:$@)\n");
	}
}

# Register plugin info
sub registerPlugin {
	my ($plugin,$info) = @_;


	# If no info, return
	if (!defined($info)) {
		print(STDERR "Plugin info not found for plugin => $plugin\n");
		return -1;
	}

	# Set real module name & save
	$info->{'Module'} = $plugin;
	push(@plugins,$info);

	# Just a bit of debug
	print(STDERR "Info: Loaded plugin => $plugin\n");

	return 0;
}



587
588
589
590
591
592
593
594
595
596
# Fake server used when calling the API directly
package wiaflos::client::fakeserver;

use DateTime;
use wiaflos::server::core::logging;

sub new {
	my $class = shift;
	return bless {}, $class;
}
Nigel Kukard's avatar
Nigel Kukard committed
597

598
599
600
601
sub map_dispatch
{
	my ($self) = @_;
}
Nigel Kukard's avatar
Nigel Kukard committed
602
603


604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
# Slightly better logging
sub log
{
	my ($self,$level,$msg,@args) = @_;

	# Check log level and set text
	my $logtxt = "UNKNOWN";
	if ($level == LOG_DEBUG) {
		$logtxt = "DEBUG";
	} elsif ($level == LOG_INFO) {
		$logtxt = "INFO";
	} elsif ($level == LOG_NOTICE) {
		$logtxt = "NOTICE";
	} elsif ($level == LOG_WARN) {
		$logtxt = "WARNING";
	} elsif ($level == LOG_ERR) {
		$logtxt = "ERROR";
	}

	# Parse message nicely
	if ($msg =~ /^(\[[^\]]+\]) (.*)/s) {
		$msg = "$1 $logtxt: $2";
	} else {
		$msg = "[CORE] $logtxt: $msg";
	}
Nigel Kukard's avatar
Nigel Kukard committed
629

630
631
632
633
	# If we have args, this is more than likely a format string & args
	if (@args > 0) {
		$msg = sprintf($msg,@args);
	}
Nigel Kukard's avatar
Nigel Kukard committed
634

635
	my $timestamp = DateTime->now();
Nigel Kukard's avatar
Nigel Kukard committed
636

637
638
	printf(STDERR "[".$timestamp->strftime("%F %T")." - $$] $msg\n",@args);
}
Nigel Kukard's avatar
Nigel Kukard committed
639
640
641



Nigel Kukard's avatar
Nigel Kukard committed
642
643
1;
# vim: ts=4