diff --git a/LICENSE b/LICENSE
index adbc062..5b43521 100644
--- a/LICENSE
+++ b/LICENSE
@@ -290,7 +290,7 @@ to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
- Tracer finds outdated running packages in your system
+ Tracer finds outdated running applications in your system
Copyright (C) 2013 Jakub Kadlčík
This program is free software; you can redistribute it and/or modify
diff --git a/README.md b/README.md
index c7b2815..3443394 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# tracer
-Tracer finds outdated running packages in your system
+Tracer finds outdated running applications in your system
How does he do it? He simply finds all packages you have modified since you boot up. Then he traces their files in the jungle of your memory, ... senses them, and finally, finds them. In the end you will get list of packages what have been running while you updated or removed them.
@@ -8,6 +8,7 @@ How does he do it? He simply finds all packages you have modified since you boot
- Supported linux distribution - There are currently supported [Fedora](http://fedoraproject.org/) and [Gentoo](http://www.gentoo.org/)
- Python interpreter
- Python [psutil](https://code.google.com/p/psutil/) module. Available [here](https://admin.fedoraproject.org/pkgdb/acls/name/python-psutil) and [here](https://packages.gentoo.org/package/dev-python/psutil). Please use testing version on gentoo.
+- Python [beautifulsoup](http://www.crummy.com/software/BeautifulSoup/bs4/doc/) module. Available [here](https://admin.fedoraproject.org/pkgdb/acls/name/python-beautifulsoup4) and [here](https://packages.gentoo.org/package/dev-python/beautifulsoup)
## Usage
### Basics
@@ -59,10 +60,18 @@ Tracer is called after every successful transaction.
vim-enhanced.i686 2:7.4.179-1.fc20
Calling tracer
- vim-X11
+ gvim
Done!
+If you cant see tracer section in your output, make sure that you don't have `plugins=0` in your `/etc/dnf/dnf.conf`.
+
## Feedback
Please report any bugs or feature requests to [issues](https://github.com/FrostyX/tracer/issues) on this repository. Pull requests are also welcome. If you rather want a talk or something, you can find me on `#gentoo.cs` or `#fedora-cs` `@freenode` or you can [mail me](mailto:frostyx@email.cz).
+
+
+## References
+-
+-
+-
diff --git a/bin/tracer.py b/bin/tracer.py
index 469cd7c..ee9c39c 100755
--- a/bin/tracer.py
+++ b/bin/tracer.py
@@ -1,7 +1,21 @@
#!/usr/bin/python
-# -*- coding: utf-8 -*-
-# Tracer finds outdated running packages in your system
-# Copyright 2013 Jakub Kadlčík
+#-*- coding: utf-8 -*-
+# tracer.py
+# Tracer finds outdated running applications in your system
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
# Enable importing modules from parent directory (tracer's root directory)
import os
@@ -10,9 +24,15 @@
import sys
import time
+import datetime
+from resources.lang import _
from resources.tracer import Tracer
from resources.args_parser import args
from resources.package import Package
+from resources.exceptions import UnsupportedDistribution
+from resources.applications import Applications
+import resources.memory as Memory
+import resources.system as System
def main(argv=sys.argv, stdin=[]):
@@ -26,12 +46,124 @@ def main(argv=sys.argv, stdin=[]):
for package in args.packages + stdin_packages:
packages.append(Package(package, time.time() if args.now else None))
- tracer = Tracer()
- tracer.specified_packages = packages
- tracer.now = args.now
- for package in set(tracer.trace_running()):
- # More times a package is updated the more times it is contained in a package list.
- print package.name
+ try:
+ tracer = Tracer()
+ tracer.specified_packages = packages
+ tracer.now = args.now
+
+ processes = tracer.trace_running()
+ if not processes: return
+ if args.interactive: _print_all_interactive(processes)
+ else: _print_all(processes)
+
+ except UnsupportedDistribution as ex:
+ print ex
+
+def _print_all(processes):
+ without_static = _exclude_type(processes, Applications.TYPES["STATIC"])
+ without_session = _exclude_type(without_static, Applications.TYPES["SESSION"])
+ for process in without_session:
+ print process.name
+
+ static_count = len(processes)-len(without_static)
+ session_count = len(without_static)-len(without_session)
+ _print_note_for_hidden(session_count, static_count)
+
+def _print_all_interactive(processes):
+ processes = list(processes) # Cause Set is not ordered
+ without_static = _exclude_type(processes, Applications.TYPES["STATIC"])
+ without_session = _exclude_type(without_static, Applications.TYPES["SESSION"])
+ static_count = len(processes)-len(without_static)
+ session_count = len(without_static)-len(without_session)
+ while True:
+ i = 1
+ l = len(str(len(without_session))) # Number of digits in processes length
+ for process in without_session:
+ n = "[{0}]".format(i).ljust(l + 2)
+ print "{} {}".format(n, process.name)
+ i += 1
+ _print_note_for_hidden(session_count, static_count)
+
+ print "\n" + _("prompt_help")
+ answer = raw_input("--> ")
+ try:
+ if answer == "q": return
+ elif int(answer) <= 0 or int(answer) > i: raise IndexError
+ print_helper(without_session[int(answer) - 1].name)
+
+ except (SyntaxError, IndexError, ValueError):
+ print _("wrong_app_number")
+
+ raw_input(_("press_enter"))
+
+def _exclude_type(processes, app_type):
+ """app_type -- see Applications.TYPES"""
+ without = []
+ for process in processes:
+ app = Applications.find(process.name)
+ if app["type"] != app_type:
+ without.append(process)
+ return without
+
+def _print_note_for_hidden(session_count, static_count):
+ if not args.quiet and (session_count > 0 or static_count > 0):
+ print "\n" + _("note_unlisted_apps")
+ if session_count > 0:
+ print _("requiring_session").format(session_count)
+
+ if static_count > 0:
+ print _("requiring_reboot").format(static_count)
+
+def print_helper(app_name):
+ try:
+ tracer = Tracer()
+ package = tracer.package_info(app_name)
+ process = Memory.process_by_name(app_name)
+ app = Applications.find(app_name)
+
+ now = datetime.datetime.fromtimestamp(time.time())
+ started = datetime.datetime.fromtimestamp(process.create_time)
+ started = now - started
+
+ started_str = ""
+ if started.days > 0:
+ started_str = str(started.days) + " days"
+ elif started.seconds >= 60 * 60:
+ started_str = str(started.seconds / (60 * 60)) + " hours"
+ elif started.seconds >= 60:
+ started_str = str(started.seconds / 60) + " minutes"
+ elif started.seconds >= 0:
+ started_str = str(started.seconds) + " seconds"
+
+ how_to_restart = _("not_known_restart")
+ if app["type"] == Applications.TYPES["DAEMON"]:
+ init = System.init_system()
+ if init == "systemd": how_to_restart = "systemctl restart {0}".format(app["name"])
+ elif init == "init": how_to_restart = "/etc/init.d/{0} restart".format(app["name"])
+
+ print _("helper").format(
+ app_name = app_name,
+ pkg_name = package.name,
+ type = app["type"].capitalize(),
+ pkg_description = package.description,
+ user = process.username,
+ time = started_str,
+ pid = process.pid,
+ how_to_restart = how_to_restart,
+ )
+
+ except AttributeError:
+ print _("app_not_running").format(app_name)
+
+
if __name__ == '__main__':
+ if args.helper:
+ print_helper(args.helper[0])
+ sys.exit()
+
+ if os.getuid() != 0:
+ print _("root_only")
+ sys.exit();
+
main()
diff --git a/data/README.md b/data/README.md
new file mode 100644
index 0000000..9946b76
--- /dev/null
+++ b/data/README.md
@@ -0,0 +1,5 @@
+## rules.xml
+Rules in `rules.xml` are intended to find a proper processes. Rules defines path in processes tree from that one, which was affected by update through parents to the process, what user knows and what actually should be restarted.
+
+## applications.xml
+Definitions in `applications.xml` have nothing to do with searching. They are used just before application is printed to the output. They can set one of several types to the application (they can be printed differently) or define specific way, how to restart the application.
diff --git a/data/applications.xml b/data/applications.xml
new file mode 100644
index 0000000..583906c
--- /dev/null
+++ b/data/applications.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/rules.xml b/data/rules.xml
new file mode 100644
index 0000000..a8cacf9
--- /dev/null
+++ b/data/rules.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/integration/dnf/plugins/tracer.py b/integration/dnf/plugins/tracer.py
index 5802bc7..f7336a8 100644
--- a/integration/dnf/plugins/tracer.py
+++ b/integration/dnf/plugins/tracer.py
@@ -1,5 +1,6 @@
#-*- coding: utf-8 -*-
-# tracer.py, calls tracer after every successful transaction.
+# tracer.py
+# Calls tracer after every successful transaction.
# Also supplies the 'tracer' command.
#
# Copyright (C) 2014 Jakub Kadlčík
diff --git a/lang/__init__.py b/lang/__init__.py
new file mode 100644
index 0000000..8d1c8b6
--- /dev/null
+++ b/lang/__init__.py
@@ -0,0 +1 @@
+
diff --git a/lang/en.py b/lang/en.py
new file mode 100644
index 0000000..846cb8a
--- /dev/null
+++ b/lang/en.py
@@ -0,0 +1,53 @@
+#-*- coding: utf-8 -*-
+# en.py
+# English localization module
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+
+import textwrap
+
+LOCALE = {
+
+ # Global
+ "root_only" : "Only root can use this application",
+ "note_unlisted_apps" : "Please note that there are:",
+ "requiring_session" : " - {0} processes requiring restarting your session (i.e. Logging out & Logging in again)",
+ "requiring_reboot" : " - {0} processes requiring reboot",
+ "unsupported_distro" : ( "You are running unsupported linux distribution\n"
+ "\n"
+ "Please visit https://github.com/FrostyX/tracer/issues\n"
+ "and create new issue called 'Unknown or unsupported linux distribution: {0}' if there isn't such.\n"
+ "\n"
+ "Don't you have an GitHub account? Please report this issue on frostyx@email.cz" ),
+
+ # Interactive
+ "prompt_help" : "Press application number for help or 'q' to quit",
+ "press_enter" : "-- Press enter to get list of applications --",
+ "wrong_app_number" : "Wrong application number",
+
+ # Helpers
+ "app_not_running" : "Application called {0} is not running",
+ "not_known_restart" : "Sorry, It's not known",
+ "helper" : textwrap.dedent("""\
+ * {app_name}
+ Package: {pkg_name}
+ Description: {pkg_description}
+ Type: {type}
+ State: {app_name} has been started by {user} {time} ago. PID - {pid}
+
+ How to restart:
+ {how_to_restart}
+ """),
+}
diff --git a/packageManagers/README.md b/packageManagers/README.md
new file mode 100644
index 0000000..49b7d06
--- /dev/null
+++ b/packageManagers/README.md
@@ -0,0 +1,10 @@
+# Package managers
+
+Every package manager module should inherit `IPackageManger` class and implement its methods:
+
+- `packages_newer_than(self, unix_time)`
+- `package_files(self, pkg_name)`
+- `package_info(self, app_name)`
+- `provided_by(self, app_name)`
+
+Also there should be unit test for every package manager. Please see [dnf test](https://github.com/FrostyX/tracer/blob/develop/tests/test_dnf.py) for example.
diff --git a/packageManagers/dnf.py b/packageManagers/dnf.py
index bdf86bd..597e21f 100644
--- a/packageManagers/dnf.py
+++ b/packageManagers/dnf.py
@@ -1,6 +1,20 @@
#-*- coding: utf-8 -*-
-"""Module to work with DNF package manager class
-Copyright 2013 Jakub Kadlčík"""
+# dnf.py
+# Module to work with DNF package manager class
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
from rpm import Rpm
diff --git a/packageManagers/dpkg.py b/packageManagers/dpkg.py
new file mode 100644
index 0000000..69aca58
--- /dev/null
+++ b/packageManagers/dpkg.py
@@ -0,0 +1,88 @@
+#-*- coding: utf-8 -*-
+# dpkg.py
+# Module to work with dpkg based package managers
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+
+from ipackageManager import IPackageManager
+from resources.package import Package
+import resources.memory as Memory
+import subprocess
+import time
+import os
+
+class Dpkg(IPackageManager):
+
+ @property
+ def dpkg_log(self): return '/var/log/dpkg.log'
+
+ def packages_newer_than(self, unix_time):
+ """
+ Returns list of packages which were modified between unix_time and present
+ Requires root permissions.
+ """
+ newer = []
+ log = open(self.dpkg_log, 'r')
+ for line in log:
+ line = line.split(" ")
+
+ if line[2] != "upgrade":
+ continue
+
+ # There actually should be %e instead of %d
+ modified = time.mktime(time.strptime(line[0] + " " + line[1], "%Y-%m-%d %H:%M:%S"))
+ if modified >= unix_time:
+ pkg_name = line[3].split(":")[0]
+ newer.append(Package(pkg_name, modified))
+ return newer
+
+ def package_files(self, pkg_name):
+ """Returns list of files provided by package"""
+ files = []
+ FNULL = open(os.devnull, 'w')
+ p = subprocess.Popen(['dpkg-query', '-L', pkg_name], stdout=subprocess.PIPE, stderr=FNULL)
+ out, err = p.communicate()
+ for file in out.split('\n')[:-1]:
+ if os.path.isfile(file):
+ files.append(file)
+ return files
+
+ def package_info(self, app_name):
+ """Returns package object with all attributes"""
+ name = self.provided_by(app_name)
+ description = None
+
+ p = subprocess.Popen(['dpkg', '-s', name], stdout=subprocess.PIPE)
+ out, err = p.communicate()
+ out = out.split('\n')
+
+ for line in out:
+ if line.startswith("Description:"):
+ description = line.split("Description:")[1].strip()
+
+ package = Package(name)
+ package.description = description
+ return package
+
+ def provided_by(self, app_name):
+ """Returns name of package which provides given application"""
+ process = Memory.process_by_name(app_name)
+ f = process.cmdline[0]
+
+ p = subprocess.Popen(['dlocate', '-S', f], stdout=subprocess.PIPE)
+ package, err = p.communicate()
+ package = package.split('\n')[0]
+
+ return package.split(':')[0]
diff --git a/packageManagers/ipackageManager.py b/packageManagers/ipackageManager.py
index 054430f..bb6a41f 100755
--- a/packageManagers/ipackageManager.py
+++ b/packageManagers/ipackageManager.py
@@ -1,6 +1,20 @@
#-*- coding: utf-8 -*-
-"""This class should be inherited by any other package manager
-Copyright 2013 Jakub Kadlčík"""
+# ipackageManager.py
+# This class should be inherited by any other package manager
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
class IPackageManager:
def packages_newer_than(self, unix_time):
@@ -13,3 +27,18 @@ def packages_newer_than(self, unix_time):
def package_files(self, pkg_name):
"""Returns list of files provided by package"""
raise NotImplementedError
+
+ def package_info(self, app_name):
+ """Returns package object with all attributes"""
+ raise NotImplementedError
+
+ def provided_by(self, app_name):
+ """Returns name of package which provides given application"""
+ raise NotImplementedError
+
+ def _pkg_name_without_version(self, pkg_name):
+ try:
+ pkg_name = pkg_name[:pkg_name.index('.')] # Cut from first . to end
+ pkg_name = pkg_name[:pkg_name.rindex('-')] # Cut from last - to end
+ except ValueError: pass
+ return pkg_name
diff --git a/packageManagers/portage.py b/packageManagers/portage.py
index 4de4591..6aab95d 100755
--- a/packageManagers/portage.py
+++ b/packageManagers/portage.py
@@ -1,9 +1,24 @@
#-*- coding: utf-8 -*-
-"""Module to work with portage package manager class
-Copyright 2013 Jakub Kadlčík"""
+# portage.py
+# Module to work with portage package manager class
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
from ipackageManager import IPackageManager
from resources.package import Package
+import resources.memory as Memory
import subprocess
import time
import os
@@ -25,9 +40,9 @@ def packages_newer_than(self, unix_time):
modified = time.mktime(time.strptime(package[0], "%a %b %d %H:%M:%S %Y"))
if modified >= unix_time:
pkg_name = package[1] # Package name with version, let's cut it off
- pkg_name = pkg_name[:pkg_name.index('.')] # Cut from first . to end
- pkg_name = pkg_name[:pkg_name.rindex('-')] # Cut from last - to end
+ pkg_name = self._pkg_name_without_version(pkg_name)
newer.append(Package(pkg_name, modified))
+
return newer
def package_files(self, pkg_name):
@@ -37,3 +52,31 @@ def package_files(self, pkg_name):
files, err = p.communicate()
return files.split('\n')[:-1]
+ def package_info(self, app_name):
+ """Returns package object with all attributes"""
+ name = self.provided_by(app_name)
+ description = None
+
+ p = subprocess.Popen(['eix', '-e', name], stdout=subprocess.PIPE)
+ out, err = p.communicate()
+ out = out.split('\n')
+
+ for line in out:
+ line = line.strip()
+ if line.startswith("Description:"):
+ description = line.split("Description:")[1].strip()
+
+ package = Package(name)
+ package.description = description
+ return package
+
+ def provided_by(self, app_name):
+ """Returns name of package which provides given application"""
+ process = Memory.process_by_name(app_name)
+ f = process.cmdline[0]
+
+ p = subprocess.Popen(['equery', '-q', 'b', f], stdout=subprocess.PIPE)
+ pkg_name, err = p.communicate()
+ pkg_name = pkg_name.split('\n')[0]
+
+ return self._pkg_name_without_version(pkg_name)
diff --git a/packageManagers/rpm.py b/packageManagers/rpm.py
index d07b3a7..9126f29 100644
--- a/packageManagers/rpm.py
+++ b/packageManagers/rpm.py
@@ -1,10 +1,25 @@
#-*- coding: utf-8 -*-
-"""Base RPM package manager class
-Copyright 2013 Jakub Kadlčík"""
+# rpm.py
+# Base RPM package manager class
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
from os import listdir
from ipackageManager import IPackageManager
from resources.package import Package
+import resources.memory as Memory
import sqlite3
import subprocess
import re
@@ -47,6 +62,34 @@ def package_files(self, pkg_name):
files, err = p.communicate()
return files.split('\n')[:-1]
+ def package_info(self, app_name):
+ """Returns package object with all attributes"""
+ name = self.provided_by(app_name)
+ description = None
+
+ p = subprocess.Popen(['rpm', '-qi', name], stdout=subprocess.PIPE)
+ out, err = p.communicate()
+ out = out.split('\n')
+
+ for line in out:
+ if line.startswith("Summary"):
+ description = line.split("Summary :")[1].strip()
+
+ package = Package(name)
+ package.description = description
+ return package
+
+ def provided_by(self, app_name):
+ """Returns name of package which provides given application"""
+ process = Memory.process_by_name(app_name)
+ f = process.exe
+
+ p = subprocess.Popen(['rpm', '-qf', f], stdout=subprocess.PIPE)
+ pkg_name, err = p.communicate()
+ pkg_name = pkg_name.split('\n')[0]
+
+ return self._pkg_name_without_version(pkg_name)
+
def _transactions_newer_than(self, unix_time):
"""
Returns list of transactions which ran between unix_time and present.
diff --git a/packageManagers/yum.py b/packageManagers/yum.py
index 197e120..7c00347 100755
--- a/packageManagers/yum.py
+++ b/packageManagers/yum.py
@@ -1,6 +1,20 @@
#-*- coding: utf-8 -*-
-"""Module to work with YUM package manager class
-Copyright 2013 Jakub Kadlčík"""
+# yum.py
+# Module to work with YUM package manager class
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
from rpm import Rpm
diff --git a/resources/applications.py b/resources/applications.py
new file mode 100644
index 0000000..93809ec
--- /dev/null
+++ b/resources/applications.py
@@ -0,0 +1,73 @@
+#-*- coding: utf-8 -*-
+# applications.py
+# Manager for applications file
+#
+# Copyright (C) 2014 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+
+from bs4 import BeautifulSoup, element
+from os.path import dirname, realpath
+parentdir = dirname(dirname(realpath(__file__)))
+
+class Applications:
+
+ DEFINITIONS = parentdir + "/data/applications.xml"
+
+ TYPES = {
+ "DAEMON" : "daemon",
+ "STATIC" : "static",
+ "SESSION" : "session",
+ "APPLICATION" : "application"
+ }
+ DEFAULT_TYPE = TYPES["APPLICATION"]
+ _apps = None
+
+ @staticmethod
+ def find(app_name):
+ if not Applications._apps:
+ Applications._load()
+
+ for app in Applications._apps:
+ if app["name"] == app_name:
+ app.setdefault('type', Applications.DEFAULT_TYPE)
+ return app
+
+ return {"name" : app_name, "type" : Applications.DEFAULT_TYPE}
+
+ @staticmethod
+ def all():
+ if not Applications._apps:
+ Applications._load()
+
+ return Applications._apps
+
+ @staticmethod
+ def _load():
+ Applications._apps = []
+ f = open(Applications.DEFINITIONS)
+ soup = BeautifulSoup(f.read())
+
+ for child in soup.applications.children:
+ if not isinstance(child, element.Tag):
+ continue
+
+ if child.name == "app":
+ Applications._apps.append(child.attrs)
+
+ if child.name == "group":
+ for app in child.findChildren():
+ app.attrs.update(child.attrs)
+ Applications._apps.append(app.attrs)
+
+ f.close();
diff --git a/resources/args_parser.py b/resources/args_parser.py
index fbfa45c..55e5c3f 100644
--- a/resources/args_parser.py
+++ b/resources/args_parser.py
@@ -1,8 +1,26 @@
+#-*- coding: utf-8 -*-
+# args_parser.py
+# Module for parsing console arguments
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+
import argparse
parser = argparse.ArgumentParser(
prog = 'tracer',
- description='Tracer finds outdated running packages in your system',
+ description='Tracer finds outdated running applications in your system',
)
parser.add_argument('packages',
@@ -11,10 +29,29 @@
help='packages that only should be traced'
)
+parser.add_argument('-i', '--interactive',
+ dest='interactive',
+ action='store_true',
+ help='run tracer in interactive mode. Print numbered applications and give helpers based on numbers'
+)
+
parser.add_argument('-n', '--now',
dest='now',
action='store_true',
help='when there are specified packages, dont look for time of their update. Use "now" instead'
)
+parser.add_argument('-q', '--quiet',
+ dest='quiet',
+ action='store_true',
+ help='do not print additional information'
+)
+
+parser.add_argument('-s', '--show',
+ nargs=1,
+ dest='helper',
+ metavar='app_name',
+ help='show helper for given application'
+)
+
args = parser.parse_args()
diff --git a/resources/exceptions.py b/resources/exceptions.py
new file mode 100644
index 0000000..cc95c88
--- /dev/null
+++ b/resources/exceptions.py
@@ -0,0 +1,27 @@
+#-*- coding: utf-8 -*-
+# exceptions.py
+# Tracer exceptions module
+#
+# Copyright (C) 2014 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+
+from lang import _
+
+class UnsupportedDistribution(OSError):
+
+ @property
+ def message(self): return _("unsupported_distro")
+
+ def __init__(self, distro):
+ OSError.__init__(self, self.message.format(distro))
diff --git a/resources/lang.py b/resources/lang.py
new file mode 100644
index 0000000..01d2591
--- /dev/null
+++ b/resources/lang.py
@@ -0,0 +1,48 @@
+#-*- coding: utf-8 -*-
+# lang.py
+# Module working with language localizations
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+
+DEFAULT_LANG = "en"
+
+from os import environ
+from os.path import dirname, realpath
+parentdir = dirname(dirname(realpath(__file__)))
+_LANG_PATH = parentdir + "/lang/"
+
+# Languages supported by system, sorted by priority
+def _system_languages():
+ lang = []
+ for l in environ.get('LANG', '').split(':'):
+ lang.append(l.split("_")[0])
+
+ lang.append(DEFAULT_LANG)
+ return lang
+
+# Import language locale (throws ImportError)
+def _locale(lang):
+ return __import__("lang.%s" % lang, fromlist=["LOCALE"]).LOCALE
+
+# Import a dictionary containing all localization lines for system language
+_LOCALE = None
+for lang in _system_languages():
+ try: _LOCALE = _locale(lang); break
+ except ImportError: pass
+
+# Whenever you want print some language-specific text, use this function
+def _(string_label):
+ try: return _LOCALE[string_label]
+ except: return string_label
diff --git a/resources/memory.py b/resources/memory.py
index 1ffd39c..d0989cc 100644
--- a/resources/memory.py
+++ b/resources/memory.py
@@ -1,8 +1,23 @@
#-*- coding: utf-8 -*-
-"""Module to work with files in memory
-Copyright 2013 Jakub Kadlčík"""
+# memory.py
+# Module to work with files in memory
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
-import psutil
+from sets import Set
+import resources.psutils as psutil
import re
def process_files(pid):
@@ -22,22 +37,38 @@ def process_files(pid):
p = psutil.Process(pid)
for mmap in p.get_memory_maps():
if re.match(combined, mmap.path):
- files.append(mmap.path)
+ file = mmap.path
- return files
+ # Doesnt matter what is after dot cause in package files there is version number after it
+ try: file = file[:file.index('.')]
+ except ValueError: pass
-def is_in_memory(regex_file, memory):
+ # Doesnt matter what is after space cause filename ends with first space
+ try: file = file[:file.index(' ')]
+ except ValueError: pass
+
+ files.append(file)
+
+ return sorted(files)
+
+def processes_using_file(file, memory):
"""
- Predicates if file is loaded in memory
+ Returns list of processes which have file loaded into memory
memory -- list given by self.processes_with_files()
- return psutil.Process if true, otherwise False
+ return list of psutil.Process
@TODO This function should be hardly optimized
"""
+ used_by = []
for process in memory:
- for file in process[1]:
- if regex_file.match(file):
- return process[0]
- return False
+ l = 0
+ r = len(process[1])
+ while l <= r:
+ m = (l + r) / 2
+ if m >= len(process[1]): break
+ if file == process[1][m]: used_by.append(process[0]); break
+ if file < process[1][m]: r = m - 1
+ else: l = m + 1
+ return used_by
def files_in_memory():
"""
@@ -47,7 +78,7 @@ def files_in_memory():
for pid in psutil.get_pid_list():
try:
files += process_files(pid)
- except psutil._error.NoSuchProcess:
+ except psutil.NoSuchProcess:
pass
return set(files)
@@ -57,13 +88,32 @@ def processes_with_files():
Returns multidimensional list with this pattern - list[psutil.Process][files]
"""
processes = []
- for pid in psutil.get_pid_list():
+ for p in all_processes():
try:
- processes.append([psutil.Process(pid), process_files(pid)])
- except psutil._error.NoSuchProcess:
- pass
- except psutil._error.AccessDenied:
- pass
+ processes.append([p, process_files(p.pid)])
+ except psutil.NoSuchProcess: pass
+ except psutil.AccessDenied: pass
return processes
+def process_by_name(name):
+ for pid in psutil.get_pid_list():
+ try:
+ p = psutil.Process(pid)
+ if p.name == name:
+ return p
+
+ except psutil.NoSuchProcess: pass
+ except psutil.AccessDenied: pass
+
+ return None
+
+def all_processes():
+ processes = Set()
+ for pid in psutil.get_pid_list():
+ try:
+ processes.add(psutil.Process(pid))
+ except psutil.NoSuchProcess: pass
+ except psutil.AccessDenied: pass
+
+ return processes
diff --git a/resources/package.py b/resources/package.py
index 141cf03..faf8b51 100644
--- a/resources/package.py
+++ b/resources/package.py
@@ -1,47 +1,46 @@
-# -*- coding: utf-8 -*-
+#-*- coding: utf-8 -*-
+# package.py
+# Represents linux package
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
class Package:
- """
- Represents single package
- Copyright 2013 Jakub Kadlčík
- """
+ """Represents linux package"""
- _name = None
- _modified = None
+ name = None
+ modified = None
+ description = None
+ pkg_type = None
def __init__(self, name, modified=None):
- self._name = name
- self._modified = modified
+ self.name = name
+ self.modified = modified
def __eq__(self, package):
"""Packages are equal when they have same name"""
return (isinstance(package, self.__class__)
- and self._name == package._name)
+ and self.name == package.name)
def __ne__(self, package):
return not self.__eq__(package)
def __repr__(self):
- return ""
+ return ""
def __str__(self):
- return self._name
+ return self.name
def __hash__(self):
- return hash(id(self))
-
- @property
- def name(self):
- return self._name
-
- @name.setter
- def name(self, name):
- self._name = name
-
- @property
- def modified(self):
- return self._modified
-
- @modified.setter
- def modified(self, name):
- self._modified = modified
+ return hash(self.name)
diff --git a/resources/psutils.py b/resources/psutils.py
new file mode 100644
index 0000000..3eb72bc
--- /dev/null
+++ b/resources/psutils.py
@@ -0,0 +1,38 @@
+#-*- coding: utf-8 -*-
+# psutils.py
+# Personally modified python-psutil package
+# https://code.google.com/p/psutil/
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+
+from psutil import *
+
+class Process(Process):
+ def __eq__(self, process):
+ """For our purposes, two processes are equal when they have same name"""
+ return (isinstance(process, self.__class__)
+ and self.name == process.name)
+
+ def __ne__(self, process):
+ return not self.__eq__(process)
+
+ def __hash__(self):
+ return hash(self.name)
+
+ @property
+ def parent(self):
+ p = super(Process, self).parent
+ p.__class__ = Process
+ return p
diff --git a/resources/rules.py b/resources/rules.py
new file mode 100644
index 0000000..23db1bc
--- /dev/null
+++ b/resources/rules.py
@@ -0,0 +1,61 @@
+#-*- coding: utf-8 -*-
+# rules.py
+# Manager for rules file
+#
+# Copyright (C) 2014 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+
+from bs4 import BeautifulSoup
+from os.path import dirname, realpath
+parentdir = dirname(dirname(realpath(__file__)))
+
+class Rules:
+
+ DEFINITIONS = parentdir + "/data/rules.xml"
+
+ ACTIONS = {
+ "CALL-PARENT" : "call-parent",
+ "PRINT" : "print",
+ }
+ _DEFAULT_ACTION = ACTIONS["CALL-PARENT"]
+ _rules = None
+
+ @staticmethod
+ def find(app_name):
+ if not Rules._rules:
+ Rules._load()
+
+ for rule in Rules._rules:
+ if rule["name"] == app_name:
+ return rule
+
+ @staticmethod
+ def all():
+ if not Rules._rules:
+ Rules._load()
+
+ return Rules._rules
+
+ @staticmethod
+ def _load():
+ Rules._rules = []
+ f = open(Rules.DEFINITIONS)
+ soup = BeautifulSoup(f.read())
+
+ for rule in soup.find_all("rule"):
+ r = rule.attrs
+ r.setdefault('action', Rules._DEFAULT_ACTION)
+ Rules._rules.append(r)
+
+ f.close();
diff --git a/resources/system.py b/resources/system.py
new file mode 100644
index 0000000..697ce6e
--- /dev/null
+++ b/resources/system.py
@@ -0,0 +1,50 @@
+#-*- coding: utf-8 -*-
+# system.py
+# Module for getting data about your operating system
+# Dont worry, only necessary data required for this application.
+# Tracer *will not* store, collect or send your data anywhere.
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+
+import os
+import platform
+import resources.psutils as psutil
+from packageManagers.dnf import Dnf
+from packageManagers.yum import Yum
+from packageManagers.portage import Portage
+from packageManagers.dpkg import Dpkg
+from resources.exceptions import UnsupportedDistribution
+
+def package_manager():
+ """Returns instance of package manager according to installed linux distribution"""
+
+ distro = platform.linux_distribution(full_distribution_name=False)[0]
+ def e(): raise UnsupportedDistribution(distro)
+
+ return {
+ 'gentoo': Portage,
+ 'fedora': Dnf,
+ 'debian': Dpkg,
+ }.get(distro, e)()
+
+def init_system():
+ """
+ Returns name of init system you are using
+ e.g. init, systemd, upstart
+ """
+
+ init = psutil.Process(1)
+ name = init.name.split(" ")[0]
+ return name
diff --git a/resources/tracer.py b/resources/tracer.py
index 0a7ecf5..5154d7e 100644
--- a/resources/tracer.py
+++ b/resources/tracer.py
@@ -1,23 +1,32 @@
-# -*- coding: utf-8 -*-
+#-*- coding: utf-8 -*-
+# tracer.py
+# Tracer finds outdated running applications in your system
+#
+# Copyright (C) 2013 Jakub Kadlčík
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
-# System modules
import re
-import psutil
-import platform
+from sets import Set
-# Tracer modules
-from packageManagers.dnf import Dnf
-from packageManagers.yum import Yum
-from packageManagers.portage import Portage
from resources.package import Package
+from resources.rules import Rules
import resources.memory as memory
+import resources.psutils as psutil
+import resources.system as system
class Tracer:
-
- """
- Tracer finds outdated running packages in your system
- Copyright 2014 Jakub Kadlčík
- """
+ """Tracer finds outdated running applications in your system"""
"""List of packages that only should be traced"""
_specified_packages = None
@@ -32,17 +41,7 @@ class Tracer:
_PACKAGE_MANAGER = None
def __init__(self):
- self._PACKAGE_MANAGER = self._PACKAGE_MANAGER()
-
- def _PACKAGE_MANAGER(self):
- """Returns instance of package manager according to installed linux distribution"""
- def e(): raise OSError("Unknown or unsupported linux distribution")
-
- distro = platform.linux_distribution(full_distribution_name=False)[0]
- return {
- 'gentoo': Portage(),
- 'fedora': Dnf(),
- }.get(distro, e)
+ self._PACKAGE_MANAGER = system.package_manager()
def _modified_packages(self):
"""Returns list of packages what tracer should care about"""
@@ -56,25 +55,44 @@ def _modified_packages(self):
packages.remove(package)
return packages
+ def package_info(self, app_name):
+ return self._PACKAGE_MANAGER.package_info(app_name)
+
def trace_running(self):
"""
- Returns list of packages which have some files loaded in memory
+ Returns list of processes which uses some files that have been modified
@TODO This function should be hardly optimized
"""
files_in_memory = memory.processes_with_files()
packages = self.specified_packages if self.specified_packages and self._now else self._modified_packages()
- modified = []
+ running = Set()
for package in packages:
for file in self._PACKAGE_MANAGER.package_files(package.name):
# Doesnt matter what is after dot cause in package files there is version number after it
- regex = re.compile('^' + re.escape(file) + "\.*")
- p = memory.is_in_memory(regex, files_in_memory)
- if p and p.create_time <= package.modified:
- modified.append(package)
- break
- return modified
+ try: file = file[:file.index('.')]
+ except ValueError: pass
+
+ for p in memory.processes_using_file(file, files_in_memory):
+ if p.create_time <= package.modified:
+ p = self._apply_rules(p)
+ running.add(p)
+ return running
+
+ def _apply_rules(self, process):
+ parent = process.parent
+ rule = Rules.find(parent.name)
+
+ if not rule or not rule["action"]:
+ return process
+
+ if rule["action"] == Rules.ACTIONS["CALL-PARENT"]:
+ return self._apply_rules(parent)
+
+ # Only PRINT action left
+ # PRINT rule is defined for parent process
+ return parent
@property
def specified_packages(self):
diff --git a/tests/__meta__.py b/tests/__meta__.py
new file mode 100644
index 0000000..dacf9d9
--- /dev/null
+++ b/tests/__meta__.py
@@ -0,0 +1,6 @@
+# Enable importing modules from parent directory (tracer's root directory)
+import os
+parentdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+os.sys.path.insert(0, parentdir)
+
+import unittest
diff --git a/tests/test-all.sh b/tests/test-all.sh
new file mode 100755
index 0000000..84e37d7
--- /dev/null
+++ b/tests/test-all.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+# Depend on: dev-python/unittest2
+
+unit2 discover
diff --git a/tests/test_applications.py b/tests/test_applications.py
new file mode 100644
index 0000000..0294ab6
--- /dev/null
+++ b/tests/test_applications.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+from __meta__ import *
+from resources.applications import Applications
+
+from bs4 import BeautifulSoup
+from os.path import dirname, realpath
+
+class TestApplications(unittest.TestCase):
+
+ def test_apps_attributes(self):
+ i = 1
+ for a in Applications.all():
+ if ("name" not in a) or len(a) <= 1:
+ self.fail("Missing name in definition #" + str(i))
+
+ if "type" in a and a["type"] not in Applications.TYPES.values():
+ self.fail("Unknown type in application: " + a["name"])
+
+ allowed_keys = ["name", "type", "helper", "rename"]
+ for key in a.keys():
+ self.assertIn(key, allowed_keys,
+ "Unsupported attribute '{0}' in application: {1}"
+ .format(key, a["name"]))
+
+ i += 1
+
+ def test_apps_duplicity(self):
+ apps = Applications.all()
+ for a in apps:
+ if self._count(a["name"], apps) > 1:
+ self.fail("Duplicate definitions for: " + a["name"])
+
+ def test_app_with_no_definition(self):
+ app_name = "NON_EXISTING_APPLICATION"
+ expected = {"name" : app_name, "type" : Applications.DEFAULT_TYPE}
+ self.assertDictEqual(expected, Applications.find(app_name))
+
+ def _count(self, app_name, apps):
+ count = 0
+ for a in apps:
+ if a["name"] == app_name:
+ count += 1
+ return count
+
+
+if __name__ == '__main__':
+ unittest.main()
+
diff --git a/tests/test_dnf.py b/tests/test_dnf.py
new file mode 100644
index 0000000..3a7391c
--- /dev/null
+++ b/tests/test_dnf.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+
+from __meta__ import *
+from packageManagers.ipackageManager import IPackageManager
+from packageManagers.dnf import Dnf
+
+class TestDnf(unittest.TestCase):
+ def setUp(self):
+ self.manager = Dnf()
+
+ def test_implements_package_manager_interface(self):
+ self.assertIsInstance(self.manager, IPackageManager, "Every package manager should inherit from IPackageManager")
+
+ def test_package_newer_than_implemented(self):
+ try: self.manager.packages_newer_than(0)
+ except NotImplementedError: self.fail("packages_newer_than() is not implemented!")
+ except Exception: pass
+
+ def test_package_info(self):
+ try: self.manager.package_info("")
+ except NotImplementedError: self.fail("package_info() is not implemented!")
+ except Exception: pass
+
+ def test_package_files_implemented(self):
+ try: self.manager.package_files("")
+ except NotImplementedError: self.fail("packages_files() is not implemented!")
+ except Exception: pass
+
+ def test_provided_by(self):
+ try: self.manager.provided_by("")
+ except NotImplementedError: self.fail("provided_by() is not implemented!")
+ except Exception: pass
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_dpkg.py b/tests/test_dpkg.py
new file mode 100644
index 0000000..de0002b
--- /dev/null
+++ b/tests/test_dpkg.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+
+from __meta__ import *
+from packageManagers.ipackageManager import IPackageManager
+from packageManagers.dpkg import Dpkg
+
+class TestDpkg(unittest.TestCase):
+ def setUp(self):
+ self.manager = Dpkg()
+
+ def test_implements_package_manager_interface(self):
+ self.assertIsInstance(self.manager, IPackageManager, "Every package manager should inherit from IPackageManager")
+
+ def test_package_newer_than_implemented(self):
+ try: self.manager.packages_newer_than(0)
+ except NotImplementedError: self.fail("packages_newer_than() is not implemented!")
+ except Exception: pass
+
+ def test_package_info(self):
+ try: self.manager.package_info("")
+ except NotImplementedError: self.fail("package_info() is not implemented!")
+ except Exception: pass
+
+ def test_package_files_implemented(self):
+ try: self.manager.package_files("")
+ except NotImplementedError: self.fail("packages_files() is not implemented!")
+ except Exception: pass
+
+ def test_provided_by(self):
+ try: self.manager.provided_by("")
+ except NotImplementedError: self.fail("provided_by() is not implemented!")
+ except Exception: pass
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_lang.py b/tests/test_lang.py
new file mode 100644
index 0000000..1025dab
--- /dev/null
+++ b/tests/test_lang.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+from __meta__ import *
+import resources.lang as Lang
+import os
+
+class TestLang(unittest.TestCase):
+
+ def test_locale_implementation(self):
+ default_locale = Lang._locale(Lang.DEFAULT_LANG)
+ for lang in self._languages():
+ locale = Lang._locale(lang)
+
+ for key, value in default_locale.items():
+ self.assertIn(key, locale, "Locale '{0}' doesn't implement '{1}' label".format(lang, key))
+
+ for key, value in locale.items():
+ self.assertIn(key, default_locale, "Locale '{0}' shouldn't implement '{1}' label".format(lang, key))
+
+ def _languages(self):
+ languages = []
+ for file in os.listdir(Lang._LANG_PATH):
+ file = file.split(".")[0]
+ if file.startswith("__") or file.endswith("__"):
+ continue
+
+ languages.append(file)
+ return set(languages)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_portage.py b/tests/test_portage.py
new file mode 100644
index 0000000..b5e85fd
--- /dev/null
+++ b/tests/test_portage.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+
+from __meta__ import *
+from packageManagers.ipackageManager import IPackageManager
+from packageManagers.portage import Portage
+
+class TestPortage(unittest.TestCase):
+ def setUp(self):
+ self.manager = Portage()
+
+ def test_implements_package_manager_interface(self):
+ self.assertIsInstance(self.manager, IPackageManager, "Every package manager should inherit from IPackageManager")
+
+ def test_package_newer_than_implemented(self):
+ try: self.manager.packages_newer_than(0)
+ except NotImplementedError: self.fail("packages_newer_than() is not implemented!")
+ except Exception: pass
+
+ def test_package_info(self):
+ try: self.manager.package_info("")
+ except NotImplementedError: self.fail("package_info() is not implemented!")
+ except Exception: pass
+
+ def test_package_files_implemented(self):
+ try: self.manager.package_files("")
+ except NotImplementedError: self.fail("packages_files() is not implemented!")
+ except Exception: pass
+
+ def test_provided_by(self):
+ try: self.manager.provided_by("")
+ except NotImplementedError: self.fail("provided_by() is not implemented!")
+ except Exception: pass
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_rules.py b/tests/test_rules.py
new file mode 100644
index 0000000..4e28081
--- /dev/null
+++ b/tests/test_rules.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+
+from __meta__ import *
+from resources.rules import Rules
+
+from bs4 import BeautifulSoup
+from os.path import dirname, realpath
+
+class TestRules(unittest.TestCase):
+
+ def test_rules_attributes(self):
+ i = 1
+ for r in Rules.all():
+ if ("name" not in r) or ("action" not in r):
+ self.fail("Missing attributes in rule #" + str(i))
+
+ if r["action"] not in Rules.ACTIONS.values():
+ self.fail("Unknown action in rule: " + r["name"])
+
+ if len(r) > 2:
+ self.fail("Unsupported attributes in rule: " + r["name"])
+
+ i += 1
+
+ def test_rules_duplicity(self):
+ rules = Rules.all()
+ for r in rules:
+ if self._count(r["name"], rules) > 1:
+ self.fail("Duplicate rules for: " + r["name"])
+
+ def test_app_with_no_rule(self):
+ self.assertIsNone(Rules.find("NON_EXISTING_APPLICATION"))
+
+ def _count(self, app_name, rules):
+ count = 0
+ for r in rules:
+ if r["name"] == app_name:
+ count += 1
+ return count
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_yum.py b/tests/test_yum.py
new file mode 100644
index 0000000..d5eeacd
--- /dev/null
+++ b/tests/test_yum.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+
+from __meta__ import *
+from packageManagers.ipackageManager import IPackageManager
+from packageManagers.yum import Yum
+
+class TestYum(unittest.TestCase):
+ def setUp(self):
+ self.manager = Yum()
+
+ def test_implements_package_manager_interface(self):
+ self.assertIsInstance(self.manager, IPackageManager, "Every package manager should inherit from IPackageManager")
+
+ def test_package_newer_than_implemented(self):
+ try: self.manager.packages_newer_than(0)
+ except NotImplementedError: self.fail("packages_newer_than() is not implemented!")
+ except Exception: pass
+
+ def test_package_info(self):
+ try: self.manager.package_info("")
+ except NotImplementedError: self.fail("package_info() is not implemented!")
+ except Exception: pass
+
+ def test_package_files_implemented(self):
+ try: self.manager.package_files("")
+ except NotImplementedError: self.fail("packages_files() is not implemented!")
+ except Exception: pass
+
+ def test_provided_by(self):
+ try: self.manager.provided_by("")
+ except NotImplementedError: self.fail("provided_by() is not implemented!")
+ except Exception: pass
+
+
+if __name__ == '__main__':
+ unittest.main()