aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAngelo Naselli <anaselli@linux.it>2014-03-08 18:07:49 +0100
committerAngelo Naselli <anaselli@linux.it>2014-03-08 18:07:49 +0100
commitb7a2fb5401c2fb44574b78598e7eefbfe6645119 (patch)
tree2cc03d911d9b02a6d7939216016ada8ebaaede98
parent036d607b90e6727ffc08026a120e2693ba7ef87f (diff)
downloadcolin-keep-b7a2fb5401c2fb44574b78598e7eefbfe6645119.tar
colin-keep-b7a2fb5401c2fb44574b78598e7eefbfe6645119.tar.gz
colin-keep-b7a2fb5401c2fb44574b78598e7eefbfe6645119.tar.bz2
colin-keep-b7a2fb5401c2fb44574b78598e7eefbfe6645119.tar.xz
colin-keep-b7a2fb5401c2fb44574b78598e7eefbfe6645119.zip
Added new logviewer based on systemd journal
-rw-r--r--Makefile.PL5
-rw-r--r--lib/AdminPanel/Module/LogViewer.pm537
-rw-r--r--lib/AdminPanel/Shared/JournalCtl.pm143
-rwxr-xr-xscripts/logviewer11
-rw-r--r--t/02-JournalCtl.t4
5 files changed, 697 insertions, 3 deletions
diff --git a/Makefile.PL b/Makefile.PL
index ea7cd17..9651a22 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -7,7 +7,7 @@ WriteMakefile(
NAME => 'AdminPanel',
AUTHOR => q{Angelo Naselli <anaselli@linux.it> - Matteo Pasotti <matteo.pasotti@gmail.com>},
VERSION_FROM => 'lib/AdminPanel/MainDisplay.pm',
- ABSTRACT => 'AdminPanel contains a generic launcher application that can run perl modules or external programs using Suse YUI abstarction.',
+ ABSTRACT => 'AdminPanel is a generic launcher application that can run perl modules or external programs using Suse YUI abstarction.',
LICENSE => 'GPL_2',
PL_FILES => {},
MIN_PERL_VERSION => 5.006,
@@ -25,12 +25,15 @@ WriteMakefile(
# AdminPanel::Shared::Locales
"Locale::gettext" => 0,
"Text::Iconv" => 0,
+ "Date::Simple" => 0,
+ "File::HomeDir" => 0,
},
EXE_FILES => [ qw( scripts/adminMouse
scripts/adminService
scripts/adminUser
scripts/apanel.pl
scripts/hostmanager
+ scripts/logviewer
scripts/mgaAddUser
) ],
dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
diff --git a/lib/AdminPanel/Module/LogViewer.pm b/lib/AdminPanel/Module/LogViewer.pm
new file mode 100644
index 0000000..dacb98f
--- /dev/null
+++ b/lib/AdminPanel/Module/LogViewer.pm
@@ -0,0 +1,537 @@
+# vim: set et ts=4 sw=4:
+package AdminPanel::Module::LogViewer;
+#============================================================= -*-perl-*-
+
+=head1 NAME
+
+AdminPanel::Module::LogViewer - Log viewer
+
+=head1 SYNOPSIS
+
+ my $logViewer = AdminPanel::Module::LogViewer->new();
+ $logViewer->start();
+
+=head1 DESCRIPTION
+
+Log viewer is a backend to journalctl, it can also load a custom
+file.
+
+
+=head1 SUPPORT
+
+You can find documentation for this module with the perldoc command:
+
+perldoc AdminPanel::Module::::LogViewer
+
+=head1 AUTHOR
+
+Angelo Naselli <anaselli@linux.it>
+
+=head1 COPYRIGHT and LICENSE
+
+Copyright (C) 2014, Angelo Naselli.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2, as
+published by the Free Software Foundation.
+
+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.
+
+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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+
+=head1 FUNCTIONS
+
+=cut
+
+use strict;
+use open OUT => ':utf8';
+
+use AdminPanel::Shared;
+use AdminPanel::Shared::Locales;
+use AdminPanel::Shared::Services qw (services);
+use AdminPanel::Shared::JournalCtl;
+
+
+use POSIX qw/strftime floor/;
+use Date::Simple ();
+use File::HomeDir qw(home);
+
+use Moose;
+use yui;
+
+extends qw( AdminPanel::Module );
+
+### TODO icon
+has '+icon' => (
+ default => "/usr/share/mcc/themes/default/logdrake-mdk.png",
+);
+
+# has '+name' => (
+# default => undef,
+# );
+
+has 'loc' => (
+ is => 'rw',
+ init_arg => undef,
+ builder => '_localeInitialize'
+);
+
+sub _localeInitialize {
+ my $self = shift();
+
+ # TODO fix domain binding for translation
+ $self->loc(AdminPanel::Shared::Locales->new(domain_name => 'libDrakX-standalone') );
+ # TODO if we want to give the opportunity to test locally add dir_name => 'path'
+}
+
+=head1 VERSION
+
+Version 1.0.0
+
+=cut
+
+our $VERSION = '1.0.0';
+
+
+my %prior = ('emerg' => 0,
+ 'alert' => 1,
+ 'crit' => 2,
+ 'err' => 3,
+ 'warning' => 4,
+ 'notice' => 5,
+ 'info' => 6,
+ 'debug' => 7);
+
+
+#=============================================================
+
+=head2 BUILD
+
+=head3 INPUT
+
+ $self: this object
+
+=head3 DESCRIPTION
+
+ The BUILD method is called after a Moose object is created,
+ in this methods Services loads all the service information.
+
+=cut
+
+#=============================================================
+sub BUILD {
+ my $self = shift;
+
+ if (! $self->name) {
+ $self->name ($self->loc->N("Log viewer"));
+ }
+}
+
+
+#=============================================================
+
+=head2 start
+
+=head3 INPUT
+
+ $self: this object
+
+=head3 DESCRIPTION
+
+ This method extends Module::start and is invoked to
+ start adminService
+
+=cut
+
+#=============================================================
+sub start {
+ my $self = shift;
+
+ $self->_logViewerPanel();
+};
+
+
+
+
+
+sub _logViewerPanel {
+ my $self = shift;
+
+ if(!$self->_warn_about_user_mode()) {
+ return 0;
+ }
+
+ my $appTitle = yui::YUI::app()->applicationTitle();
+
+ ## set new title to get it in dialog
+ yui::YUI::app()->setApplicationTitle($self->name);
+ ## set icon if not already set by external launcher
+ yui::YUI::app()->setApplicationIcon($self->icon);
+
+ my $factory = yui::YUI::widgetFactory;
+ my $optFactory = yui::YUI::optionalWidgetFactory;
+
+ # Create Dialog
+ my $dialog = $factory->createMainDialog;
+
+ # Start Dialog layout:
+ my $vbox = $factory->createVBox( $dialog );
+ $vbox->setWeight($yui::YD_HORIZ, 7);
+ my $align = $factory->createAlignment($vbox, $yui::YAlignCenter, $yui::YAlignUnchanged);
+ $factory->createLabel( $align, $self->loc->N("A tool to monitor your logs"), 1, 0 );
+
+ #### matching
+ my $hbox = $factory->createHBox($vbox);
+ my $matchingInputField = $factory->createInputField($hbox, $self->loc->N("Matching"));
+ $factory->createHSpacing($hbox, 1);
+
+ #### not matching
+ my $notMatchingInputField = $factory->createInputField($hbox, $self->loc->N("but not matching"));
+ $matchingInputField->setWeight($yui::YD_HORIZ, 1);
+ $notMatchingInputField->setWeight($yui::YD_HORIZ, 1);
+
+ $hbox = $factory->createHBox($vbox);
+ my $frame = $factory->createFrame($hbox, $self->loc->N("Options"));
+ $frame->setStretchable($yui::YD_HORIZ, 1);
+
+ #### search
+ my $searchButton = $factory->createPushButton($hbox, $self->loc->N("search"));
+ #$searchButton->setStretchable($yui::YD_HORIZ, 0);
+
+ $frame->setWeight($yui::YD_HORIZ, 2);
+ #$searchButton->setWeight($yui::YD_HORIZ, 1);
+
+
+ $hbox = $factory->createHBox($frame);
+# $align = $factory->createLeft($hbox);
+
+ $frame = $factory->createFrame($hbox, "");
+ $frame->setWeight($yui::YD_HORIZ, 1);
+
+ my $vbox1 = $factory->createVBox( $frame );
+
+ #### units
+ my $unitsFrame = $factory->createCheckBoxFrame($vbox1, $self->loc->N("Select a unit"), 1);
+ $unitsFrame->setNotify(1);
+ my $units = $factory->createComboBox ( $unitsFrame, "" );
+ my $itemCollection = new yui::YItemCollection;
+
+ yui::YUI::app()->busyCursor();
+ my ($l, $active_services) = AdminPanel::Shared::Services::services();
+
+ foreach (@{$active_services}) {
+ my $serviceName = $_;
+ my $item = new yui::YItem($serviceName);
+ $itemCollection->push($item);
+ $item->DISOWN();
+ }
+ $units->addItems($itemCollection);
+ yui::YUI::app()->normalCursor();
+
+ $factory->createVSpacing($vbox1, 1);
+ #### lastBoot
+ my $lastBoot = $factory->createCheckBox( $vbox1, $self->loc->N("Last boot") , 1 );
+ $lastBoot->setNotify(1);
+
+ $frame = $factory->createFrame($hbox, $self->loc->N("Calendar"));
+ $frame->setWeight($yui::YD_HORIZ, 1);
+ $vbox1 = $factory->createVBox( $frame );
+ #### since and until
+ my $sinceDate;
+ my $sinceFrame = $factory->createCheckBoxFrame($vbox1, $self->loc->N("Since"), 1);
+ $sinceFrame->setNotify(1);
+ my $untilDate;
+ $factory->createVSpacing($vbox1, 1);
+ my $untilFrame = $factory->createCheckBoxFrame($vbox1, $self->loc->N("Until"), 1);
+ $untilFrame->setNotify(1);
+ if ($optFactory->hasDateField()) {
+ $sinceDate = $optFactory->createDateField($sinceFrame, "");
+ my $day = strftime "%F", localtime;
+ $sinceDate->setValue($day);
+ $untilDate = $optFactory->createDateField($untilFrame, "");
+ $untilDate->setValue($day);
+ }
+ else {
+ $sinceFrame->enable(0);
+ $untilFrame->enable(0);
+ }
+
+ $frame = $factory->createFrame($hbox, $self->loc->N("Priority"));
+ $frame->setWeight($yui::YD_HORIZ, 1);
+ $vbox1 = $factory->createVBox( $frame );
+ #### priority
+ # From
+ my $priorityFromFrame = $factory->createCheckBoxFrame($vbox1, $self->loc->N("From"), 1);
+ $priorityFromFrame->setNotify(1);
+ my $priorityFrom = $factory->createComboBox ( $priorityFromFrame, "" );
+ $itemCollection->clear();
+
+ my @pr = ('emerg', 'alert', 'crit', 'err',
+ 'warning', 'notice', 'info', 'debug');
+ foreach (@pr) {
+ my $item = new yui::YItem($_);
+ if ( $_ eq 'emerg' ) {
+ $item->setSelected(1);
+ }
+ $itemCollection->push($item);
+ $item->DISOWN();
+ }
+ $priorityFrom->addItems($itemCollection);
+
+ $factory->createVSpacing($vbox1, 1);
+ # To
+ my $priorityToFrame = $factory->createCheckBoxFrame($vbox1, $self->loc->N("To"), 1);
+ $priorityToFrame->setNotify(1);
+ my $priorityTo = $factory->createComboBox ( $priorityToFrame, "" );
+ $itemCollection->clear();
+
+ foreach (@pr) {
+ my $item = new yui::YItem($_);
+ if ( $_ eq 'debug' ) {
+ $item->setSelected(1);
+ }
+ $itemCollection->push($item);
+ $item->DISOWN();
+ }
+ $priorityTo->addItems($itemCollection);
+
+ #### create log view object
+ my $logView = $factory->createLogView($vbox, $self->loc->N("Log content"), 10, 0);
+
+
+ ### NOTE CheckBoxFrame doesn't honoured his costructor checked value for his children
+ $unitsFrame->setValue(0);
+ $sinceFrame->setValue(0);
+ $untilFrame->setValue(0);
+ $priorityFromFrame->setValue(0);
+ $priorityToFrame->setValue(0);
+
+ # buttons on the last line
+ $align = $factory->createRight($vbox);
+ $hbox = $factory->createHBox($align);
+ my $aboutButton = $factory->createPushButton($hbox, $self->loc->N("About") );
+ $align = $factory->createRight($hbox);
+ $hbox = $factory->createHBox($align);
+ my $saveButton = $factory->createPushButton($hbox, $self->loc->N("Save"));
+ my $quitButton = $factory->createPushButton($hbox, $self->loc->N("Quit"));
+
+ # End Dialof layout
+
+ while(1) {
+ my $event = $dialog->waitForEvent();
+ my $eventType = $event->eventType();
+
+ #event type checking
+ if ($eventType == $yui::YEvent::CancelEvent) {
+ last;
+ }
+ elsif ($eventType == $yui::YEvent::WidgetEvent) {
+ # widget selected
+ my $widget = $event->widget();
+ if ($widget == $quitButton) {
+ last;
+ }
+ elsif($widget == $aboutButton) {
+ my $license = $self->loc->N($AdminPanel::Shared::License);
+
+ AdminPanel::Shared::AboutDialog({ name => $self->name,
+ version => $self->VERSION,
+ copyright => $self->loc->N("Copyright (C) %s Mageia community", '2014'),
+ license => $license,
+ comments => $self->loc->N("Log viewer is a systemd journal viewer."),
+ website => 'http://www.mageia.org',
+ website_label => $self->loc->N("Mageia"),
+ authors => "Angelo Naselli <anaselli\@linux.it>\nMatteo Pasotti <matteo.pasotti\@gmail.com>",
+ translator_credits =>
+ #-PO: put here name(s) and email(s) of translator(s) (eg: "John Smith <jsmith@nowhere.com>")
+ $self->loc->N("_: Translator(s) name(s) & email(s)\n")}
+ );
+ }
+ elsif($widget == $saveButton) {
+ if ($logView->lines()) {
+ $self->_save($logView);
+ }
+ else {
+ AdminPanel::Shared::warningMsgBox($self->loc->N("Empty log found"));
+ }
+ }
+ elsif ($widget == $searchButton) {
+ yui::YUI::app()->busyCursor();
+ $dialog->startMultipleChanges();
+ $logView->clearText();
+ my %log_opts;
+ if ($lastBoot->isChecked()) {
+ $log_opts{this_boot} = 1;
+ }
+ if ($unitsFrame->value()) {
+ $log_opts{unit} = $units->value();
+ }
+ if ($sinceFrame->value()) {
+ $log_opts{since} = $sinceDate->value();
+ }
+ if ($untilFrame->value()) {
+ $log_opts{until} = $untilDate->value();
+# TODO check date until > date since
+ }
+ if ($priorityFromFrame->value() || $priorityToFrame->value()) {
+ my $prio = $priorityFrom->value();
+ $prio .= "..".$priorityTo->value() if ($priorityToFrame->value());
+ $log_opts{priority} = $prio;
+# TODO enabling right using checkBoxes
+ }
+ my $log = $self->_search(\%log_opts);
+print " log lines: ". scalar (@{$log}) ."\n";
+# TODO check on log line number what to do if too big? and adding a progress bar?
+ $self->_parse_content({'matching' => $matchingInputField->value(),
+ 'noMatching' => $notMatchingInputField->value(),
+ 'log' => $log,
+ 'logView' => $logView,
+ }
+ );
+ $dialog->recalcLayout();
+ $dialog->doneMultipleChanges();
+ yui::YUI::app()->normalCursor();
+ }
+ elsif ($widget == $lastBoot) {
+ yui::YUI::ui()->blockEvents();
+ if ($lastBoot->isChecked()) {
+ #last boot overrrides until and since
+ $sinceFrame->setValue(0);
+ $untilFrame->setValue(0);
+ }
+ yui::YUI::ui()->unblockEvents();
+ }
+ elsif ($widget == $sinceFrame) {
+ yui::YUI::ui()->blockEvents();
+ if ($sinceFrame->value()) {
+ #disabling last boot that overrrides until and since
+ $lastBoot->setValue(0);
+ }
+ yui::YUI::ui()->unblockEvents();
+ }
+ elsif ($widget == $untilFrame) {
+ yui::YUI::ui()->blockEvents();
+ if ($untilFrame->value()) {
+ #disabling last boot that overrrides until and since
+ $lastBoot->setValue(0);
+ }
+ yui::YUI::ui()->unblockEvents();
+ }
+
+ }
+ }
+ $dialog->destroy();
+
+ #restore old application title
+ yui::YUI::app()->setApplicationTitle($appTitle) if $appTitle;
+}
+
+
+
+sub _warn_about_user_mode {
+ my $self = shift;
+
+ my $title = $self->loc->N("Running in user mode");
+ my $msg = $self->loc->N("You are launching this program as a normal user.\n".
+ "You will not be able to read system logs which you do not have rights to,\n".
+ "but you may still browse all the others.");
+
+ # $EUID: effective user identifier
+ if(($> != 0) and (!ask_YesOrNo($title, $msg))) {
+ # TODO add Privileges?
+ return 0;
+ }
+
+ return 1;
+}
+
+
+## Save as
+#
+# $logView: log Widget
+#
+##
+sub _save {
+ my ($self, $logView) = @_;
+
+ yui::YUI::app()->busyCursor();
+
+ my $outFile = yui::YUI::app()->askForSaveFileName(home(), "*", $self->loc->N("Save as.."));
+ if ($outFile) {
+ open(OF, ">".$outFile);
+ print OF $logView->logText();
+ close OF;
+ }
+
+ yui::YUI::app()->normalCursor();
+}
+
+## Search call back
+sub _search {
+ my ($self, $log_opts) = @_;
+
+$DB::single = 1;
+ my $log = AdminPanel::Shared::JournalCtl->new(%{$log_opts});
+ my $all = $log->getLog();
+
+print " _search \n";
+ return $all;
+}
+
+## _parse_content
+#
+# $info : HASH cotaining
+#
+# matching: string to match
+# notMatching: string to skip
+# log: ARRAY REF to log content
+# logViewer: logViewer Widget
+#
+##
+sub _parse_content {
+ my ($self, $info) = @_;
+
+ $DB::single = 1;
+
+ my $ey = "";
+ my $en = "";
+
+ if( exists($info->{'matching'} ) ){
+ $ey = $info->{'matching'};
+ }
+ if( exists($info->{'noMatching'} ) ){
+ $en = $info->{'noMatching'};
+ }
+
+ $ey =~ s/ OR /|/ if ($ey);
+ $ey =~ s/^\*$// if ($ey);
+ $en =~ s/^\*$/.*/ if ($en);
+
+ my $test;
+
+ if ($en && !$ey) {
+ $test = sub { $_[0] !~ /$en/ };
+ }
+ elsif ($ey && !$en) {
+ $test = sub { $_[0] =~ /$ey/ };
+ }
+ elsif ($ey && $en) {
+ $test = sub { $_[0] =~ /$ey/ && $_[0] !~ /$en/ };
+ }
+ else {
+ $test = sub { $_[0] };
+ }
+
+ foreach (@{$info->{log}}) {
+ $info->{logView}->appendLines($_) if $test->($_);
+ }
+
+}
+
+
+1
diff --git a/lib/AdminPanel/Shared/JournalCtl.pm b/lib/AdminPanel/Shared/JournalCtl.pm
new file mode 100644
index 0000000..f95e8d6
--- /dev/null
+++ b/lib/AdminPanel/Shared/JournalCtl.pm
@@ -0,0 +1,143 @@
+# vim: set et ts=4 sw=4:
+package AdminPanel::Shared::JournalCtl;
+
+#============================================================= -*-perl-*-
+
+=head1 NAME
+
+AdminPanel::Shared::JournalCtl - journalctl perl wrapper
+
+=head1 SYNOPSIS
+
+ my $log = AdminPanel::Shared::JournalCtl->new();
+ my @log_content = $log->get();
+
+=head1 DESCRIPTION
+
+This module wraps journalctl allowing some running options and provides the
+output log content.
+
+=head1 SUPPORT
+
+You can find documentation for this module with the perldoc command:
+
+perldoc AdminPanel::Shared::JournalCtl
+
+
+=head1 AUTHOR
+
+Angelo Naselli <anaselli@linux.it>
+
+=head1 COPYRIGHT and LICENSE
+
+Copyright (C) 2014, Angelo Naselli.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2, as
+published by the Free Software Foundation.
+
+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.
+
+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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+=head1 METHODS
+
+=cut
+
+
+use diagnostics;
+use strict;
+
+use Moose;
+
+
+has 'this_boot' => (
+ is => 'rw',
+ isa => 'Int',
+ default => 0,
+);
+
+has 'since' => (
+ is => 'rw',
+ isa => 'Str',
+ default => "",
+);
+
+has 'until' => (
+ is => 'rw',
+ isa => 'Str',
+ default => "",
+);
+
+has 'priority' => (
+ is => 'rw',
+ isa => 'Str',
+ default => "",
+);
+
+has 'unit' => (
+ is => 'rw',
+ isa => 'Str',
+ default => "",
+);
+
+#=============================================================
+
+=head2 getLog
+
+=head3 INPUT
+
+Input_Parameter: in_par_description
+
+=head3 OUTPUT
+
+\@content: ARRAYREF containing the log content.
+
+=head3 DESCRIPTION
+
+This methods gets the log using the provided options
+
+=cut
+
+#=============================================================
+
+sub getLog {
+ my $self = shift;
+
+ my $params = "--no-pager -q";
+ if ($self->this_boot == 1) {
+ $params .= " -b";
+ }
+ if ($self->since ne "") {
+ $params .= " --since=".$self->since;
+ }
+ if ($self->until ne "") {
+ $params .= " --until=".$self->until;
+ }
+ if ($self->unit ne "") {
+ $params .= " --unit=".$self->unit;
+ }
+ if ($self->priority ne "") {
+ $params .= " --priority=".$self->priority;
+ }
+
+ $ENV{'PATH'} = '/usr/sbin:/usr/bin';
+ my $jctl = "/usr/bin/journalctl ". $params;
+
+ # TODO remove or add to log
+ print " Running " . $jctl ."\n";
+ my @content = `$jctl`;
+
+ return \@content;
+}
+
+no Moose;
+__PACKAGE__->meta->make_immutable;
+
+
+1;
diff --git a/scripts/logviewer b/scripts/logviewer
new file mode 100755
index 0000000..b36c519
--- /dev/null
+++ b/scripts/logviewer
@@ -0,0 +1,11 @@
+#!/usr/bin/perl
+
+use strict;
+
+use AdminPanel::Module::LogViewer;
+
+my $logviewer = AdminPanel::Module::LogViewer->new({icon => "/usr/share/mcc/themes/default/service-mdk.png",
+ });
+$logviewer->start();
+
+1;
diff --git a/t/02-JournalCtl.t b/t/02-JournalCtl.t
index 2fd6710..f5796a6 100644
--- a/t/02-JournalCtl.t
+++ b/t/02-JournalCtl.t
@@ -8,7 +8,7 @@ BEGIN {
use_ok( 'AdminPanel::Shared::JournalCtl' ) || print "JournalCtl failed!\n";
}
-ok( my $o = AdminPanel::Shared::JournalCtl->new(), 'create');
-ok( my $c = $o->get(), 'gets_log' );
+ ok( my $o = AdminPanel::Shared::JournalCtl->new(this_boot=>1,), 'create');
+ ok( my $c = $o->getLog(), 'gets_log' );
done_testing;