3 -------------------------------------------------------------------------------
4 Copyright (C) 2005, 2006 Sylvain Fourmanoit
6 Some ideas were taken from a initial coding effort by Cameron Daniel
9 Released under the GPL, version 2.
11 Permission is hereby granted, free of charge, to any person obtaining a copy
12 of this software and associated documentation files (the "Software"), to
13 deal in the Software without restriction, including without limitation the
14 rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
15 sell copies of the Software, and to permit persons to whom the Software is
16 furnished to do so, subject to the following conditions:
18 The above copyright notice and this permission notice shall be included in
19 all copies of the Software and its documentation and acknowledgment shall be
20 given in the documentation and software packages that this Software was
23 THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
26 THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
27 IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
28 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 -------------------------------------------------------------------------------
30 This is the adesklet's installer script. Have a look at adesklets
31 documentation for details.
33 # -----------------------------------------------------------------------------
41 print 'Threading support is REQUIRED by adesklets_installer'
44 import os
, os
.path
, re
, tarfile
, md5
, StringIO
, cStringIO
, copy
, time
45 from urllib
import urlopen
46 from xml
.dom
.minidom
import parseString
48 # -----------------------------------------------------------------------------
49 class Driver(threading
.Thread
):
51 Driver embed the installer core logic into a single, reusable
52 monothreaded component. It will be reused here by GUI-derivated classes.
54 I am not a big fan of multithreaded applications, but this is needed
55 for clean integration with most widgets toolkits these days.
58 def __init__(self
, logfile
):
60 self
.path
= os
.path
.join(os
.getenv('HOME',''), '.desklets')
61 self
.ready
= threading
.Event()
64 def descriptions(self
):
66 Returns a preformatted desklets description, suitable
67 for monospace printing., in desklets name alphabetic order.
69 desc
= ['%15s %5s (%s)' % (k
, v
['version'], v
['status'])
70 for k
, v
in self
.iteritems()]
71 desc
.sort(lambda x
, y
: cmp(x
.strip().lower(), y
.strip().lower()))
75 def extract_tag(entry
):
76 def extract_text(tag
):
77 if not tag
.hasChildNodes():
78 tag
= tag
.getAttributeNode('href')
79 return tag
.firstChild
.wholeText
.encode('ascii')
80 return [extract_text(entry
.getElementsByTagName(tag
)[0])
81 for tag
in ('title', 'link', 'generator')]
82 def extract_url(link
):
83 if 'prdownloads.sourceforge.net' in link
:
84 return ('http://dl.sourceforge.net/sourceforge/' +
86 re
.findall('adesklets/(.+)\?download', link
)[0])
91 return sum([int(v
)*100**(2-i
)
92 for v
, i
in zip(ver
.split('.'), xrange(3))])
93 return version(x
[1])-version(y
[1])
95 # Fetch and organize the data
97 print >> self
.log
, 'Retrieving data online...',
101 dict(([('version', title
.split()[1])] +
102 [('url', extract_url(link
))] +
103 [tuple(generator
.split(':'))])))
104 for title
, link
, generator
in
106 for entry
in parseString(
107 urlopen('http://adesklets.sourceforge.net/desklets.atom').read()
108 ).getElementsByTagName('entry')]])
109 print >> self
.log
, 'OK'
111 # Compare to what's installed, and set the status
113 print >> self
.log
, 'Checking locally installed desklets...',
115 if os
.path
.isdir(self
.path
):
116 inst
= [item
.split('-')
117 for item
in os
.listdir(self
.path
)
118 if os
.path
.isdir(os
.path
.join(self
.path
, item
))]
119 inst
.sort(by_version
)
126 if inst
[desklet
] == self
[desklet
]['version']:
127 self
[desklet
]['status']='installed'
129 self
[desklet
]['status']='out of date'
131 self
[desklet
]['status']='uninstalled'
132 print >> self
.log
, 'OK'
135 def install(self
, desklet
):
137 Perform a desklet installation
139 print >> self
.log
, 'Downloading %s desklet...' % desklet
,
141 f
= cStringIO
.StringIO(urlopen(self
[desklet
]['url']).read())
142 print >> self
.log
, 'OK'
143 print >> self
.log
, 'Verifying download integrity...',
145 if md5
.new(f
.getvalue()).hexdigest() != self
[desklet
]['md5']:
146 raise RuntimeError('bad download checksum')
147 print >> self
.log
, 'OK'
148 print >> self
.log
, 'Opening the downloaded archive...',
150 tar
= tarfile
.open(mode
='r|bz2', fileobj
=f
, name
='archive')
151 print >> self
.log
, 'OK'
152 print >> self
.log
, 'Extracting the archive...',
154 if not os
.path
.isdir(self
.path
): os
.mkdir(self
.path
)
155 for tarinfo
in tar
: tar
.extract(tarinfo
, path
=self
.path
)
156 self
[desklet
]['status'] = 'installed'
157 print >> self
.log
, """OK
159 desklet %(name)s version %(version)s was installed in:
161 Change to this directory, see the README,
162 and follow the instructions.
163 %(sep)s""" % {'name': desklet
,
164 'version': self
[desklet
]['version'],
165 'path': '%s/%s-%s' % (self
.path
, desklet
, self
[desklet
]['version']),
168 def __init__(self
, logfile
=sys
.stdout
):
169 self
.cond
= threading
.Condition()
173 self
.ready_state
= False
174 self
.desklets
= self
.Desklets(logfile
)
175 threading
.Thread
.__init
__(self
)
178 Whenever it returns True, the GUI instance need to
179 refresh its desklets lists (it takes care of initial load too)
181 if not self
.ready_state
:
182 if self
.desklets
.ready
.isSet():
183 self
.ready_state
= True
186 def ready(self
, block
=False):
188 Returns True if Driver is ready to process command. It can be used
189 in non-blocking (default) or blocking mode. In blocking mode, it
190 will eventually block and wait for Driver to be ready before
194 self
.desklets
.ready
.wait()
196 return self
.desklets
.ready
.isSet()
199 """Traceback tracker"""
200 print >> self
.desklets
.log
, \
201 '\n!!! An error occured during the operation !!!'
202 traceback
.print_exc(file=self
.desklets
.log
)
203 self
.desklets
.ready
.set()
206 """Main thread loop"""
215 if not self
.op
: self
.cond
.wait()
221 if hasattr(self
, '_'+op
):
222 self
.desklets
.ready
.clear()
223 getattr(self
, '_'+op
)(**kw
)
224 self
.desklets
.ready
.set()
228 def command(self
, op
, **kw
):
230 Execute a given command. For instance:
232 Driver.command('install', desklet='mydesklet')
234 NOTE: *DO NOT* call the commands directly
242 def _install(self
, desklet
):
243 self
.ready_state
=False # <= Used to signal a possible
244 self
.desklets
.install(desklet
) # refresh of desklets states
245 def _quit(self
): self
.alive
= False
247 # -----------------------------------------------------------------------------
250 UI is a base class for the installer interfaces.
252 class Log(StringIO
.StringIO
):
257 self
.lock
= threading
.Lock()
259 StringIO
.StringIO
.__init
__(self
)
260 def write(self
, string
):
262 StringIO
.StringIO
.write(self
, string
)
267 pool() => data OR None
269 If the logged information from the driver changed since last call,
270 It will return the full log.
273 if self
.lock
.acquire(False):
275 result
=copy
.deepcopy(self
.getvalue())
281 # Connect and run components
283 self
.log
= self
.Log()
284 self
.driver
= Driver(self
.log
)
289 except KeyboardInterrupt: print '(Interrupted)'
291 self
.driver
.command('quit')
295 """Main GUI loop: need to be overriden in children"""
298 # -----------------------------------------------------------------------------
300 """Very bare interface for any terminal: should always work"""
301 def Log(self
): return sys
.stdout
304 # Just output a list of choice, wait for the user, then repeat
305 # if the choice was not to exit in the first place.
309 self
.driver
.ready(block
=True)
310 desklets
= [(i
, desc
)
312 in zip(xrange(len(self
.driver
.desklets
)),
313 self
.driver
.desklets
.descriptions())]
314 print '%(sep)s\nAvailable desklets\n%(sep)s' % { 'sep': '-'*30 }
315 print '\n'.join(['%3d) %s' % item
for item
in desklets
])
316 print ' q) %15s' % 'Quit'
317 answer
= self
.input('Enter your selection > ')
318 if dict(desklets
).has_key(answer
):
319 self
.driver
.command('install',
320 desklet
=dict(desklets
)[answer
].split()[0])
322 self
.driver
.ready(block
=True)
323 answer
= self
.input('<<<Press q, to quit>>> ')
325 def input(self
, prompt
):
327 val
= raw_input(prompt
)
330 print '(End of File)'
333 if val
.strip().lower() == 'q':
337 # -----------------------------------------------------------------------------
342 """Tk-based interface"""
344 # Initialize the various widgets
346 self
.win
= Tkinter
.Tk()
347 self
.win
.resizable(False, False)
348 self
.win
.title('adesklets installer')
350 f
= Tkinter
.Frame(self
.win
)
352 s
= Tkinter
.Scrollbar(f
)
353 s
.pack(side
=Tkinter
.RIGHT
, fill
=Tkinter
.Y
)
354 self
.listbox
= Tkinter
.Listbox(f
, yscrollcommand
=s
.set,
355 font
="fixed", width
=40)
356 self
.listbox
.pack(fill
=Tkinter
.Y
)
357 s
.config(command
=self
.listbox
.yview
)
359 f
= Tkinter
.Frame(self
.win
)
361 self
.binstall
= Tkinter
.Button(f
, \
362 text
='Waiting for online description',
363 state
=Tkinter
.DISABLED
,
364 command
=self
.install
)
365 self
.binstall
.pack(side
=Tkinter
.LEFT
)
366 w
= Tkinter
.Button(f
, text
='Quit',command
=self
.win
.quit
)
367 w
.pack(side
=Tkinter
.RIGHT
)
369 f
= Tkinter
.Frame(self
.win
)
370 f
.pack(fill
=Tkinter
.Y
)
371 s
= Tkinter
.Scrollbar(f
)
372 s
.pack(side
=Tkinter
.RIGHT
, fill
=Tkinter
.Y
)
373 self
.logtext
= Tkinter
.Text(f
, yscrollcommand
=s
.set,
374 wrap
=None,width
=50, height
=6)
375 self
.logtext
.pack(fill
=Tkinter
.BOTH
)
376 s
.config(command
=self
.logtext
.yview
)
378 # Then, register a pooling callback, and start the main loop
380 self
.win
.after(0, self
.pool
)
383 def refresh_list(self
):
384 self
.listbox
.delete(0, Tkinter
.END
)
385 for desc
in self
.driver
.desklets
.descriptions():
386 self
.listbox
.insert(Tkinter
.END
, desc
)
387 self
.binstall
.config(text
='Select a desklet')
391 Periodic pooling method that refreshes the GUI
393 # Refresh the desklet list
395 if self
.driver
.refreshed(): self
.refresh_list()
397 # Refresh the installation button (state and text)
399 s
= self
.listbox
.curselection()
401 desklet
= self
.listbox
.get(s
[0])
402 if self
.desklet
!= desklet
:
403 self
.desklet
= desklet
404 self
.binstall
.config(
406 ' '.join(self
.desklet
.split()[:2]))
407 if self
.driver
.ready():
408 self
.binstall
.config(state
=Tkinter
.ACTIVE
)
410 # Refresh the log output
412 text
= self
.log
.pool()
414 self
.logtext
.config(state
=Tkinter
.NORMAL
)
415 self
.logtext
.delete(1.0, Tkinter
.END
)
416 self
.logtext
.insert(Tkinter
.END
, text
)
417 self
.logtext
.config(state
=Tkinter
.DISABLED
)
419 # Call again the routine in 1/10th of a second
421 self
.win
.after(100, self
.pool
)
424 """Button installation callback"""
425 self
.binstall
.config(state
=Tkinter
.DISABLED
)
426 self
.driver
.command('install', desklet
=self
.desklet
.split()[0])
429 """Just a fallback wrapper"""
432 except Tkinter
.TclError
:
433 print 'Could not instanciate the Tk interface'
435 except ImportError: pass
437 # -----------------------------------------------------------------------------
441 class _cursesGUI(UI
):
442 """Curses-based interface"""
444 class TextZone(object):
446 Curses is fairly low-level, so let's start by defining our
449 def __init__(self
, *args
):
452 self
.sh
, self
.sw
= args
457 self
.win
= curses
.newwin(self
.sh
-self
.sy
, self
.w
,
461 self
.pad
= curses
.newpad(self
.h
-2, self
.w
-2)
465 def refresh(self
, data
= None):
466 """Refresh the widget content, as needed"""
469 if type(data
) is type(''):
470 self
.lines
= data
.splitlines()
473 for i
, line
in zip(range(len(self
.lines
)), self
.lines
):
474 self
.pad
.addstr(i
, 0, line
)
477 self
.pad
.refresh(self
.cur
, 0,
478 self
.sy
+1, self
.sx
+1,
479 self
.sh
-2, self
.sw
-1)
480 def status(self
, status
):
481 """Set the widget status"""
483 self
.win
.addstr(0, 2, '[ %s ]' % status
)
488 """Toggle widget in active/inactive mode"""
489 self
.hl
= (0, curses
.A_REVERSE
)[self
.hl
== 0]
492 def scroll(self
, delta
):
493 """Perform a scroll"""
494 new
= self
.cur
+ delta
497 elif new
>= len(self
.lines
):
498 new
= len(self
.lines
)-1
500 if len(self
.lines
)>1:
501 self
.pad
.addstr(self
.cur
, 0, self
.lines
[self
.cur
])
502 self
.pad
.addstr(new
, 0, self
.lines
[new
], self
.hl
)
508 Pool the keyboard: scrolling actions are self-contained,
509 selection, widget circulation and quitting are not.
513 if c
== curses
.KEY_UP
or c
== ord('8'):
515 elif c
== curses
.KEY_DOWN
or c
== ord('2'):
517 elif c
== curses
.KEY_PPAGE
or c
== ord('9'):
518 self
.scroll((self
.sy
-self
.sh
)/2)
519 elif c
== curses
.KEY_NPAGE
or c
== ord('3'):
520 self
.scroll((self
.sh
-self
.sy
)/2)
523 elif c
== curses
.KEY_ENTER
or c
== ord('\n'):
525 elif c
== ord('q') or c
== 27:
529 curses
.wrapper(self
._run
)
532 # Check out terminal real-estate
534 if scr
.getmaxyx()[0]<20 or scr
.getmaxyx()[1]<50:
535 raise curses
.error('Terminal size should be at least 50x20')
538 # Instanciate our two widgets: a desklets pad and a log pad
540 dpad
= self
.TextZone(100, scr
.getmaxyx()[1],
542 scr
.getmaxyx()[0]-10, scr
.getmaxyx()[1])
543 lpad
= self
.TextZone(200, scr
.getmaxyx()[1],
544 scr
.getmaxyx()[0]-10, 0,
545 scr
.getmaxyx()[0], scr
.getmaxyx()[1])
547 # Start with the desklets pad active
550 dpad
.status('Waiting for desklets description')
553 # Now loop indefinitively
556 # Refresh the desklet list
558 if self
.driver
.refreshed():
560 dpad
.refresh(self
.driver
.desklets
.descriptions())
562 # Refresh the log output
564 text
= self
.log
.pool()
565 if text
is not None: lpad
.refresh(text
)
567 # React to keyboard...
570 if action
is not None:
571 # ...By changing the active TextZone
573 for item
in (dpad
, lpad
): item
.toggle()
574 pad
= (dpad
, lpad
)[pad
is dpad
]
575 # ... By performing the installation, as needed
576 elif action
== 'select' and pad
is dpad
:
577 desklet
= pad
.lines
[pad
.cur
].split()[0]
578 if (self
.driver
.desklets
.has_key(desklet
) and
579 self
.driver
.ready()):
580 pad
.status('Installing...')
581 self
.driver
.command('install', desklet
=desklet
)
582 #... And of course by exiting as needed
583 elif action
== 'quit':
588 """Just a fallback wrapper"""
592 print 'Could not instanciate the curses interface'
594 except ImportError: pass
596 # -----------------------------------------------------------------------------
597 if __name__
== '__main__':
599 # Parse the command line
601 from optparse
import OptionParser
602 def version(option
, opt
, value
, parser
):
604 Copyright (C) 2005, 2006 Sylvain Fourmanoit <syfou@users.sourceforge.net>
605 This is free software; see the source for copying conditions. There is NO
606 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
607 """ % (parser
.get_prog_name(), os
.getenv('ADESKLETS_VERSION', ''))
611 p
.add_option('-v', '--version', action
='callback', callback
=version
,
612 help='print out the version info and exit')
613 p
.add_option('-r', '--raw', action
='store_true', dest
='raw',
614 help='force the use of the raw terminal interface ' +
615 '(always available)')
616 p
.add_option('-c', '--curses', action
='store_true', dest
='curses',
617 help='force the use of the curses interface, when available')
618 p
.add_option('-t', '--tk', action
='store_true', dest
='Tk',
619 help='force the use of the Tk interface, when available')
620 p
.set_defaults(raw
=False, curses
=False, Tk
=False)
621 opts
, args
= p
.parse_args()
623 # Just instanciate the right GUI to start up the application
624 # In order, we try to instanciate the Tk, curses and raw
625 # interface, falling to the next in case of unavailability or
626 # initialization errors
628 select
= opts
.raw
+ opts
.curses
+ opts
.Tk
629 for ui
in ('Tk', 'curses', 'raw'):
630 if (globals().has_key('%sGUI' % ui
) and
631 (getattr(opts
, ui
) or select
==0 or ui
=='raw')):
632 if globals()['%sGUI' % ui
](): break
634 # -----------------------------------------------------------------------------