-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdevilish.py
More file actions
executable file
·382 lines (292 loc) · 13.4 KB
/
devilish.py
File metadata and controls
executable file
·382 lines (292 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
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
332
333
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
369
370
371
372
373
374
375
376
377
378
379
380
381
382
#!/usr/bin/env python
# Author:
# Iye Cba <iye {dot} cba {at} gmail {dot} com>
# https://github.com/iye/devilish
# devilish.py - PyGTK App for monitoring log files realtime. It uses inotify to
# detect file modifications and when an appended line has a string you are
# interested in, it will alert you via your notification daemon and with an icon
# in the tray.
# License:
# 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 3 of the License, or
# (at your option) any later version.
#
# 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., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
# Icons taken from Gnome Icon Theme
#TODO: right click on icontray displays "Quit" option
#TODO: configparser sucks, deletes comments when saving setting to config file, replace.
#TODO: "Copy to clipboard" option when right clicking on the log treeview
#TODO: coding style, docstings...
import gtk
import pyinotify
import pynotify
import ConfigParser
import os
class Devilish:
def __init__(self):
gladefilename = "devilish.glade"
builder = gtk.Builder()
builder.add_objects_from_file(gladefilename,
["window1", "statusicon1", "liststore1", "image1", "image2"])
builder.connect_signals(self)
#widgets
self.window = builder.get_object('window1')
self.liststore = builder.get_object('liststore1')
self.modelfilter = self.liststore.filter_new()
self.statusicon = builder.get_object('statusicon1')
#Specify the function that determines which rows to filter out and which ones to display
self.modelfilter.set_visible_func(self.filtertree, data=None)
self.treeview = builder.get_object('treeview1')
#Assign the filter as our tree's model
self.treeview.set_model(self.modelfilter)
self.searchbox = builder.get_object('entry1')
#needed for starting and stoping pyinotify from any method
self.wm = None
self.notifier = None
#Handler of the opened log file
self.file = None
#File path and name
self.filename = None
#filterwordlist will contain the strings to search in each line added to the log.
self.filterwordlist = None
# Load icons to display in statusicon
# 0 if face-devilish.png (aka nothing new in the log)
# 1 if face-angry.png (aka something new in the log)
self.icon_in_tray = 0
self.icon_allfine_pixbuf = gtk.gdk.pixbuf_new_from_file("icons/face-devilish.png")
self.icon_alert_pixbuf = gtk.gdk.pixbuf_new_from_file("icons/face-angry.png")
#Show notify daemon bubble. Set to "True" here but set from config file later.
self.show_notify_bubble = True
#Load setting from config.cfg
self.load_settings()
def load_settings(self):
#Load config file
config = ConfigParser.RawConfigParser()
config.read(['config.cfg', os.path.expanduser('~/.config/pylogwatch/myapp.cfg')])
#Path of file to open
self.filename = config.get("Main", "logfilePath")
#Filter strings, convert string to list
var = config.get("Main", "filterStrings")
var = var.strip('\"')
self.filterwordlist = var.split('\", \"')
self.show_notify_bubble = config.getboolean("Main", "notifybubble")
#Open filterdialog on Menu->Edit->"Search Strings"
def on_filter_words_dialog_open(self, widget):
filterdialog = FilterDialog(self)
#Filter for the quick search box
def filtertree(self, model, iter, data):
searchboxtext = self.searchbox.get_text()
if (searchboxtext == ""):
return True
logline = str(model.get_value(iter, 0))
if (logline.find(searchboxtext) > -1):
return True
else:
return False
#Called when the quick search box entry1 has some modification
def on_entry1_changed(self, widget):
self.modelfilter.refilter()
#On/Off button
def on_togglebutton1_toggled(self, widget, data=None):
if self.notifier.isAlive(): self.notifier.stop()
else: self.watch_log()
#Clear listore on click
def on_clearbutton_clicked(self, widget, data=None):
self.liststore.clear()
#Check if file specified in config file exists, if not, show File Chooser dialog.
def startup_file_selection(self, filepath):
try:
tryfile = open(filepath)
self.filename = tryfile.name
except:
self.filename = None
dialog = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL,
gtk.MESSAGE_INFO, gtk.BUTTONS_CLOSE,
"The file you choosed could not be opened."
" Check permissions on the file.")
dialog.set_title("Error")
dialog.run()
dialog.destroy()
pass
if self.filename == None:
self.on_open_menu_item_activate(self)
else:
self.watch_log()
def main(self):
#This makes threads work in gtk. Pyinotify thread wont work without this
gtk.gdk.threads_init()
self.window.show()
#File selection dialog will show up only if logfilePath is pointing to non existant file.
self.startup_file_selection(self.filename)
gtk.main()
def on_window1_destroy(self, widget, data=None):
#Stop pyinotifier thread
try: self.notifier.stop()
except: pass
gtk.main_quit()
return False
#What to do when the icon in tray is pressed.
def on_statusicon1_button_press_event(self, widget, data=None):
self.hide_show_window()
def hide_show_window(self):
#visible = self.window.get_property("visible")
visible = self.window.is_active()
#if visible:
if visible == True:
self.window.hide()
self.statusicon.set_from_pixbuf(self.icon_allfine_pixbuf)
else:
self.window.show()
self.window.deiconify()
self.statusicon.set_from_pixbuf(self.icon_allfine_pixbuf)
#Capture Iconify event (iconify=minimize)
def on_window_state_event(self, widget, event):
if event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED:
if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
#window was minimized if it arrives here
self.window.hide()
self.statusicon.set_from_pixbuf(self.icon_allfine_pixbuf)
#Select log file to monitor.
def on_open_menu_item_activate(self, widget, data=None):
filechooser = FileChooser(self)
#Open log file and start pyinotify thread.
def watch_log(self):
try:
self.file = open(self.filename)
except:
print "Error opening log file"
self.on_open_menu_item_activate(self)
self.file.seek(0,2)
self.wm = pyinotify.WatchManager()
self.notifier = pyinotify.ThreadedNotifier(self.wm, EventHandler())
self.notifier.start()
self.wm.add_watch(self.filename, pyinotify.IN_MODIFY)
def show_about_dialog(self, widget):
aboutdlg = AboutDialog()
#called when modification is detected by pyinotify
def log_change_action(self,event):
#read line in log and append to liststore if it has wanted string
#file.readline will return empty string if it reachs EOF.
line = self.file.readline()
while line != "":
for item in self.filterwordlist:
if line.find(item) > 0:
line = line.rstrip('\n')
linetime=line[0:15]
linelog=line[16:]
#Change icon in tray to one showing a warning
if self.icon_in_tray == 0:
self.statusicon.set_from_pixbuf(self.icon_alert_pixbuf)
#if window is active, don't show notifications
window_isactive = self.window.is_active()
if (self.show_notify_bubble == True and
window_isactive != True):
n = pynotify.Notification(linetime, linelog)
try:
n.show()
except Exception:
pass
#Code to autoscroll treeview
#app.liststore.append([linelog, linetime]) #uncomment this
# and disable following 3 lines to disable autoscroll
row_iter = app.liststore.append([linelog, linetime])
path = app.liststore.get_path(row_iter)
self.treeview.scroll_to_cell(path)
break
line = self.file.readline()
class EventHandler(pyinotify.ProcessEvent):
def process_IN_MODIFY(self, event):
#what to do in "modify" event
app.log_change_action(event)
class AboutDialog():
def __init__(self):
gladefilename = "devilish.glade"
builder = gtk.Builder()
builder.add_objects_from_file(gladefilename, ["aboutdialog1"])
self.aboutdlg = builder.get_object("aboutdialog1")
self.aboutdlg.connect("response", self.hide_about)
self.aboutdlg.show()
def hide_about(self, widget, data=None):
self.aboutdlg.hide()
return True
class FileChooser():
def __init__(self, parent_window):
#To be able to access members of parent window
self._parent_window = parent_window
chooser = gtk.FileChooserDialog(title="Open Log File..."
, action=gtk.FILE_CHOOSER_ACTION_OPEN
, buttons=(gtk.STOCK_CANCEL
, gtk.RESPONSE_CANCEL
, gtk.STOCK_OPEN, gtk.RESPONSE_OK))
response = chooser.run()
if response == gtk.RESPONSE_OK:
self._parent_window.filename = chooser.get_filename()
#stop notifier if working
if self._parent_window.notifier != None: self._parent_window.notifier.stop()
else:
self._parent_window.filename = None
chooser.destroy()
self._parent_window.on_open_menu_item_activate(self)
if self._parent_window.filename: self._parent_window.watch_log()
chooser.destroy()
class FilterDialog():
#TODO: Maybe I should just ask users to open config.cfg
# and let them edit the prefs there :)
def __init__(self, parent_window):
#Load glade file
gladefilename = "devilish.glade"
builder = gtk.Builder()
builder.add_objects_from_file(gladefilename,
["dialog1", "textview1", "checkbutton_notify"])
builder.connect_signals(self)
self.filterwordsdialog = builder.get_object("dialog1")
self.filterwords_textview = builder.get_object("textview1")
self.ckbutton_notify = builder.get_object("checkbutton_notify")
#To be able to access members of parent window
self._parent_window = parent_window
#Set the show notify baloon checkbox to current state
if self._parent_window.show_notify_bubble == True:
self.ckbutton_notify.set_active(gtk.TRUE)
else:
self.ckbutton_notify.set_active(gtk.FALSE)
#Convert list to string so we can then convert it to gtk.textbuffer.
#Gtk.textview uses gtk.textbuffer for showing text.
try:
filterlist_string = '\n'.join(map(str, self._parent_window.filterwordlist))
except:
print "Error, could not convert Filter List to String"
self.filterwords_textview.get_buffer().set_text(filterlist_string)
self.filterwordsdialog.show()
def on_button_accept_dialog1_clicked(self, widget):
textbuffer = self.filterwords_textview.get_buffer()
string_of_buffer = textbuffer.get_text(*textbuffer.get_bounds())
self._parent_window.filterwordlist = string_of_buffer.split("\n")
self.filterwordsdialog.destroy()
#Set show_notify_bubble variable to state indicated by checkbox
self._parent_window.show_notify_bubble = self.ckbutton_notify.get_active()
#Save filters to config file
config = ConfigParser.RawConfigParser()
config.read(['config.cfg', os.path.expanduser('~/.config/devilish/config.cfg')])
var = '\"' + string_of_buffer + '\"'
var = var.replace('\n','\", \"')
config.set('Main', 'filterStrings', var)
#Save show_notify_bubble state to config
config.set('Main', 'notifybubble', self._parent_window.show_notify_bubble)
#?? fix, is this writting to the config.cfg in current dir when maybe the
#one in ./config/devilish is being used.
with open('config.cfg', 'wb') as configfile:
config.write(configfile)
def on_button_dialog1_cancel_clicked(self, widget):
self.filterwordsdialog.destroy()
if __name__ == "__main__":
app = Devilish()
app.main()