Package bitfling ::
Module bitfling
|
|
1
2
3
4
5
6
7
8
9
10 """This is the BitFling client
11
12 It acts as an XML-RPC server over SSH. The UI consists of a tray icon (Windows)
13 or a small icon (Linux, Mac) that you can click on to get the dialog."""
14
15
16 import sys
17 import cStringIO
18 import os
19 import random
20 import sha
21 import thread
22 import fnmatch
23 import socket
24 import threading
25 import time
26 from xmlrpclib import Fault, Binary
27
28
29 import wx
30 import wx.html
31 import wx.lib.newevent
32 import wx.lib.masked.textctrl
33 import wx.lib.mixins.listctrl
34
35
36 import paramiko
37
38
39 try:
40 import native.usb as usb
41 except ImportError:
42 usb=None
43 import usbscan
44 import comscan
45 import commport
46
47 import guihelper
48 import xmlrpcstuff
49 import version
50
51 ID_CONFIG=wx.NewId()
52 ID_LOG=wx.NewId()
53 ID_RESCAN=wx.NewId()
54 ID_EXIT=wx.NewId()
55
56
57 XmlServerEvent, EVT_XMLSERVER = wx.lib.newevent.NewEvent()
58
59 guithreadid=thread.get_ident()
60
61
62 if guihelper.IsMSWindows(): parentclass=wx.TaskBarIcon
63 else: parentclass=wx.Frame
64
66
68 self.mw=mw
69 self.menu=menu
70 iconfile="bitfling.png"
71 if parentclass is wx.Frame:
72 parentclass.__init__(self, None, -1, "BitFling Window", size=(32,32), style=wx.FRAME_TOOL_WINDOW)
73 self.genericinit(iconfile)
74 else:
75 parentclass.__init__(self)
76 self.windowsinit(iconfile)
77
78 self.leftdownpos=0,0
79
80
81 wx.EVT_MENU(menu, ID_EXIT, self.OnExit)
82
83
85 if parentclass is wx.Frame:
86 self.Close(True)
87 else:
88 self.RemoveIcon()
89 self.Destroy()
90
92 print "I would do config at this point"
93
95 print "I would do log at this point"
96
98 print "I would do help at this point"
99
101 print "I would do rescan at this point"
102
105
111
120
122 if guihelper.IsMSWindows():
123 self.leftdownpos=0
124 else:
125 self.leftdownpos=evt.GetPosition()
126 self.motionorigin=self.leftdownpos
127
129 if not evt.Dragging():
130 return
131 if evt.RightIsDown() or evt.MiddleIsDown():
132 return
133 if not evt.LeftIsDown():
134 return
135 self.leftdownpos=None
136 x,y=evt.GetPosition()
137 xdelta=x-self.motionorigin[0]
138 ydelta=y-self.motionorigin[1]
139 screenx,screeny=self.GetPositionTuple()
140 self.MoveXY(screenx+xdelta, screeny+ydelta)
141
143 bitmap=wx.Bitmap(guihelper.getresourcefile(iconfile), wx.BITMAP_TYPE_PNG)
144 icon=wx.EmptyIcon()
145 icon.CopyFromBitmap(bitmap)
146 self.SetIcon(icon, "BitFling")
147 wx.EVT_TASKBAR_RIGHT_UP(self, self.OnRButtonUp)
148 wx.EVT_TASKBAR_LEFT_UP(self, self.OnLButtonUp)
149
150 wx.EVT_TASKBAR_LEFT_DOWN(self, self.OnLeftDown)
151
153 self.SetCursor(wx.StockCursor(wx.CURSOR_HAND))
154 bitmap=wx.Bitmap(guihelper.getresourcefile(iconfile), wx.BITMAP_TYPE_PNG)
155 bit=wx.StaticBitmap(self, -1, bitmap)
156 self.Show(True)
157 wx.EVT_RIGHT_UP(bit, self.OnRButtonUp)
158 wx.EVT_LEFT_UP(bit, self.OnLButtonUp)
159 wx.EVT_MOTION(bit, self.OnMouseMotion)
160 wx.EVT_LEFT_DOWN(bit, self.OnLeftDown)
161 self.bit=bit
162
163 -class ConfigPanel(wx.Panel, wx.lib.mixins.listctrl.ColumnSorterMixin):
164
166 wx.Panel.__init__(self, parent, id)
167 self.mw=mw
168 vbs=wx.BoxSizer(wx.VERTICAL)
169
170
171 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "General"), wx.HORIZONTAL)
172 bs.Add(wx.StaticText(self, -1, "Fingerprint"), 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
173 self.fingerprint=wx.TextCtrl(self, -1, "a", style=wx.TE_READONLY, size=(300,-1))
174 bs.Add(self.fingerprint, 0, wx.EXPAND|wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
175 bs.Add(wx.StaticText(self, -1, ""), 0, wx.ALL, 5)
176 bs.Add(wx.StaticText(self, -1, "Port"), 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
177 self.porttext=wx.StaticText(self, -1, "<No Port>")
178 bs.Add(self.porttext, 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
179 vbs.Add(bs, 0, wx.EXPAND|wx.ALL, 5)
180
181
182 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Authorization"), wx.VERTICAL)
183 hbs=wx.BoxSizer(wx.HORIZONTAL)
184 butadd=wx.Button(self, wx.NewId(), "Add ...")
185 hbs.Add(butadd, 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
186 hbs.Add(wx.StaticText(self, -1, ""), 0, wx.ALL, 5)
187 self.butedit=wx.Button(self, wx.NewId(), "Edit ...")
188 self.butedit.Enable(False)
189 hbs.Add(self.butedit, 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
190 hbs.Add(wx.StaticText(self, -1, ""), 0, wx.ALL, 5)
191 self.butdelete=wx.Button(self, wx.NewId(), "Delete")
192 self.butdelete.Enable(False)
193 hbs.Add(self.butdelete, 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
194 bs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
195
196 wx.EVT_BUTTON(self, butadd.GetId(), self.OnAddAuth)
197 wx.EVT_BUTTON(self, self.butedit.GetId(), self.OnEditAuth)
198 wx.EVT_BUTTON(self, self.butdelete.GetId(), self.OnDeleteAuth)
199
200
201 self.authlist=wx.ListCtrl(self, wx.NewId(), style=wx.LC_REPORT|wx.LC_SINGLE_SEL)
202 self.authlist.InsertColumn(0, "User")
203 self.authlist.InsertColumn(1, "Allowed Addresses")
204 self.authlist.InsertColumn(2, "Expires")
205 self.authlist.SetColumnWidth(0, 300)
206 self.authlist.SetColumnWidth(1, 300)
207 self.authlist.SetColumnWidth(2, 100)
208 bs.Add(self.authlist, 1, wx.EXPAND|wx.ALL, 5)
209
210 vbs.Add(bs, 1, wx.EXPAND|wx.ALL, 5)
211 self.itemDataMap={}
212 wx.lib.mixins.listctrl.ColumnSorterMixin.__init__(self,3)
213
214 wx.EVT_LIST_ITEM_ACTIVATED(self.authlist, self.authlist.GetId(), self.OnEditAuth)
215 wx.EVT_LIST_ITEM_SELECTED(self.authlist, self.authlist.GetId(), self.OnAuthListItemFondled)
216 wx.EVT_LIST_ITEM_DESELECTED(self.authlist, self.authlist.GetId(), self.OnAuthListItemFondled)
217 wx.EVT_LIST_ITEM_FOCUSED(self.authlist, self.authlist.GetId(), self.OnAuthListItemFondled)
218
219
220 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Devices"), wx.VERTICAL)
221 buttoggle=wx.Button(self, wx.NewId(), "Toggle Allowed")
222 bs.Add(buttoggle, 0, wx.ALL, 5)
223 self.devicelist=wx.ListCtrl(self, wx.NewId(), style=wx.LC_REPORT|wx.LC_SINGLE_SEL)
224 self.devicelist.InsertColumn(0, "Allowed")
225 self.devicelist.InsertColumn(1, "Name")
226 self.devicelist.InsertColumn(2, "Available")
227 self.devicelist.InsertColumn(3, "Description")
228 self.devicelist.SetColumnWidth(0, 100)
229 self.devicelist.SetColumnWidth(1, 300)
230 self.devicelist.SetColumnWidth(2, 100)
231 self.devicelist.SetColumnWidth(3, 300)
232 bs.Add(self.devicelist, 1, wx.EXPAND|wx.ALL, 5)
233
234 vbs.Add(bs, 1, wx.EXPAND|wx.ALL, 5)
235
236 self.setupauthorization()
237 self.SortListItems()
238
239 self.SetSizer(vbs)
240 self.SetAutoLayout(True)
241
243 pos=-1
244 if itemnum in self.itemDataMap:
245
246
247 for i in range(self.authlist.GetItemCount()):
248 if self.authlist.GetItemData(i)==itemnum:
249 pos=i
250 break
251 assert pos!=-1
252
253 self.mw.icacache={}
254 v=self.mw.authinfo[itemnum]
255 username=v[0]
256 expires=v[2]
257 addresses=" ".join(v[3])
258 if pos<0:
259 pos=self.authlist.GetItemCount()
260 self.authlist.InsertStringItem(pos, username)
261 else:
262 self.authlist.SetStringItem(pos, 0, username)
263 self.authlist.SetStringItem(pos, 2, `expires`)
264 self.authlist.SetStringItem(pos, 1, addresses)
265 self.authlist.SetItemData(pos, itemnum)
266 self.itemDataMap[itemnum]=(username, addresses, expires)
267
269 "Used by the ColumnSorter mixin"
270 return self.authlist
271
273 dict={}
274 items=[]
275 for i in range(1000):
276 if self.mw.config.HasEntry("user-"+`i`):
277 username,password,expires,addresses=self.mw.config.Read("user-"+`i`).split(":")
278 expires=int(expires)
279 addresses=addresses.split()
280 dict[i]=username,password,expires,addresses
281 items.append(i)
282 self.mw.authinfo=dict
283 for i in items:
284 self._updateauthitemmap(i)
285
286
288 dlg=AuthItemDialog(self, "Add Entry")
289 if dlg.ShowModal()==wx.ID_OK:
290 username,password,expires,addresses=dlg.GetValue()
291 for i in range(1000):
292 if i not in self.mw.authinfo:
293 self.mw.config.Write("user-"+`i`, "%s:%s:%d:%s" % (username, password, expires, " ".join(addresses)))
294 self.mw.config.Flush()
295 self.mw.authinfo[i]=username,password,expires,addresses
296 self._updateauthitemmap(i)
297 self.SortListItems()
298 break
299 dlg.Destroy()
300
308
310 "Finds the selected item in a listctrl since the wx methods don't actually work"
311 i=-1
312 while True:
313 nexti=listctrl.GetNextItem(i, state=wx.LIST_STATE_SELECTED)
314 if nexti<0:
315 break
316 i=nexti
317 return i
318 return None
319
321 "Called whenever list items are selected, unselectected or similar fondling"
322 selitem=self._getselectedlistitem(self.authlist)
323 self.butedit.Enable(selitem is not None)
324 self.butdelete.Enable(selitem is not None)
325
327 "Called to edit the currently selected entry"
328 item=self._getselectedlistitem(self.authlist)
329 key=self.authlist.GetItemData(item)
330 username,password,expires,addresses=self.mw.authinfo[key]
331 dlg=AuthItemDialog(self, "Edit Entry", username=username, password=password, expires=expires, addresses=addresses)
332 if dlg.ShowModal()==wx.ID_OK:
333 username,password,expires,addresses=dlg.GetValue()
334 self.mw.authinfo[key]=username,password,expires,addresses
335 self._updateauthitemmap(key)
336 dlg.Destroy()
337
339
340 _password_sentinel="\x01\x02\x03\x04\x05\x06\x07\x08"
341
342 - def __init__(self, parent, title, username="New User", password="", expires=0, addresses=[]):
343 wx.Dialog.__init__(self, parent, -1, title, style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
344
345 p=self
346 gs=wx.FlexGridSizer(4, 2, 5, 5)
347 gs.AddGrowableCol(1)
348 gs.AddGrowableRow(3)
349
350 gs.Add(wx.StaticText(p, -1, "Username/Email"))
351 self.username=wx.TextCtrl(self, -1, username)
352 gs.Add(self.username,0, wx.EXPAND)
353 gs.Add(wx.StaticText(p, -1, "Password"))
354 self.password=wx.TextCtrl(self, -1, "", style=wx.TE_PASSWORD)
355 self.origpassword=password
356 if len(password): self.password.SetValue(self._password_sentinel)
357 gs.Add(self.password, 0, wx.EXPAND)
358 gs.Add(wx.StaticText(p, -1, "Expires"))
359 self.expires=wx.lib.masked.textctrl.TextCtrl(p, -1, "", autoformat="EUDATETIMEYYYYMMDD.HHMM")
360 gs.Add(self.expires)
361 gs.Add(wx.StaticText(p, -1, "Allowed Addresses"))
362 self.addresses=wx.TextCtrl(self, -1, "\n".join(addresses), style=wx.TE_MULTILINE)
363 gs.Add(self.addresses, 1, wx.EXPAND)
364
365
366 vbs=wx.BoxSizer(wx.VERTICAL)
367 vbs.Add(gs,1, wx.EXPAND|wx.ALL, 5)
368 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5)
369 vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5)
370
371 self.SetSizer(vbs)
372 vbs.Fit(self)
373
374
376
377 salt="".join([chr(random.randint(0,127)) for x in range(8)])
378 saltstr="".join(["%02x" % (ord(x),) for x in salt])
379
380 val=sha.new(salt+string)
381
382 return "$".join([saltstr, val.hexdigest()])
383
384
393
394 -class MainWindow(wx.Frame):
395
396 - def __init__(self, parent, id, title):
397 self.taskwin=None
398 wx.Frame.__init__(self, parent, id, title, style=wx.RESIZE_BORDER|wx.SYSTEM_MENU|wx.CAPTION)
399
400 sys.excepthook=self.excepthook
401
402 self.authinfo={}
403 self.icacache={}
404
405
406 cfgstr='bitfling'
407 if guihelper.IsMSWindows():
408 cfgstr="BitFling"
409 self.config=wx.Config(cfgstr, style=wx.CONFIG_USE_LOCAL_FILE)
410
411
412 wx.GetApp().SetAppName(cfgstr)
413 wx.GetApp().SetVendorName(cfgstr)
414
415 self.setuphelp()
416
417 wx.EVT_CLOSE(self, self.CloseRequested)
418
419 panel=wx.Panel(self, -1)
420
421 bs=wx.BoxSizer(wx.VERTICAL)
422
423 self.nb=wx.Notebook(panel, -1)
424 bs.Add(self.nb, 1, wx.EXPAND|wx.ALL, 5)
425 bs.Add(wx.StaticLine(panel, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5)
426
427 gs=wx.GridSizer(1,4, 5, 5)
428
429 for name in ("Rescan", "Hide", "Help", "Exit" ):
430 but=wx.Button(panel, wx.NewId(), name)
431 setattr(self, name.lower(), but)
432 gs.Add(but)
433 bs.Add(gs,0,wx.ALIGN_CENTRE|wx.ALL, 5)
434
435 panel.SetSizer(bs)
436 panel.SetAutoLayout(True)
437
438
439 self.configpanel=ConfigPanel(self, self.nb)
440 self.nb.AddPage(self.configpanel, "Configuration")
441 self.lw=guihelper.LogWindow(self.nb)
442 self.nb.AddPage(self.lw, "Log")
443
444 wx.EVT_BUTTON(self, self.hide.GetId(), self.OnHideButton)
445 wx.EVT_BUTTON(self, self.help.GetId(), self.OnHelpButton)
446 wx.EVT_BUTTON(self, self.exit.GetId(), self.OnExitButton)
447
448 EVT_XMLSERVER(self, self.OnXmlServerEvent)
449
450 self.xmlrpcserver=None
451 wx.CallAfter(self.StartIfICan)
452
453 - def setuphelp(self):
454 """Does all the nonsense to get help working"""
455 import wx.html
456
457 wx.FileSystem_AddHandler(wx.ZipFSHandler())
458
459 self.helpcontroller=wx.html.HtmlHelpController()
460 self.helpcontroller.AddBook(guihelper.getresourcefile("bitpim.htb"))
461 self.helpcontroller.UseConfig(self.config, "help")
462
463
464
465
466
467
468 - def IsConnectionAllowed(self, peeraddr, username=None, password=None):
469 """Verifies if a connection is allowed
470
471 If username and password are supplied (as should be the case if calling this method
472 before executing some code) then they are checked as being from a valid address
473 as well.
474
475 If username and password are not supplied then this method checks if any
476 of the authentication rules allow a connection from the peeraddr. This form
477 is used immediately after calling accept() on a socket, but before doing
478 anything else."""
479
480
481
482 v=(peeraddr[0], username, password)
483 if username is not None and password is None:
484 self.Log("%s: No password supplied for user %s" % (peeraddr, `username`))
485 assert False, "No password supplied"
486 return False
487 print "ica of "+`v`
488 val=self.icacache.get(v, None)
489 if val is not None:
490 allowed, expires = val
491 if allowed:
492 if self._has_expired(expires):
493 msg="Connection from %s no longer allowed due to expiry" % (peeraddr[0],)
494 if username is not None:
495 msg+=". Username "+`username`
496 self.Log(msg)
497 return False
498 return True
499 return False
500
501 ret_allowed=False
502 ret_expiry=0
503 for uname, pwd, expires, addresses in self.authinfo.values():
504
505 if not self._does_address_match(peeraddr[0], addresses):
506 continue
507
508 if username is not None:
509 if username!=uname:
510 continue
511
512 if not self._verify_password(password, pwd):
513 self.Log("Wrong password supplied for user %s from %s" % (`username`, peeraddr[0]))
514 continue
515
516 ret_expiry=max(ret_expiry, expires)
517 ret_allowed=True
518
519 if not ret_allowed:
520 if username is not None:
521 self.Log("No valid credentials for user %s from %s" % (username, peeraddr[0]))
522 else:
523 self.Log("No defined users for address "+`peeraddr`)
524
525
526 self.icacache[v]=ret_allowed, ret_expiry
527 return self.IsConnectionAllowed(peeraddr, username, password)
528
529 - def _verify_password(self, password, pwd):
530 """Returns True if password matches pwd
531 @param password: password supplied by user
532 @param pwd: password as we store it (salt $ hash)"""
533 salt,hash=pwd.split("$", 1)
534
535 x=""
536 for i in range(0, len(salt), 2):
537 x+=chr(int(salt[i:i+2], 16))
538 salt=x
539
540 str=[]
541 str.extend([ord(x) for x in salt])
542 str.extend([ord(x) for x in password])
543 val=sha.new("".join([chr(x) for x in str]))
544 print password, pwd, val.hexdigest(), val.hexdigest()==hash
545 return val.hexdigest()==hash
546
547 - def _does_address_match(self, peeraddr, addresses):
548 """Returns if the peeraddr matches any of the supplied addresses"""
549
550 for addr in addresses:
551
552 if peeraddr==addr: return True
553
554 if '*' in addr or '?' in addr or '[' in addr:
555 if fnmatch.fnmatch(peeraddr, addr):
556 return True
557
558
559 ips=[]
560 try:
561 ips=socket.getaddrinfo(addr, None)
562 except:
563 pass
564 for _, _, _, _, ip in ips:
565 if peeraddr==ip[0]:
566 return True
567 return False
568
569 - def _has_expired(self, expires):
570 if expires==0:
571 return False
572 if time.time()>expires:
573 return True
574 return False
575
576 - def CloseRequested(self, evt):
577 if evt.CanVeto():
578 self.Show(False)
579 evt.Veto()
580 return
581 self.taskwin.GoAway()
582 evt.Skip()
583 sys.excepthook=sys.__excepthook__
584
585 - def OnXmlServerEvent(self, msg):
586 if msg.cmd=="log":
587 self.Log(msg.data)
588 elif msg.cmd=="logexception":
589 self.LogException(msg.data)
590 else:
591 assert False, "bad message "+`msg`
592 pass
593
594
597
600
604
605 - def Log(self, text):
606 if thread.get_ident()!=guithreadid:
607 wx.PostEvent(self, XmlServerEvent(cmd="log", data=text))
608 else:
609 self.lw.log(text)
610
611 - def LogException(self, exc):
612 if thread.get_ident()!=guithreadid:
613
614 wx.PostEvent(self, XmlServerEvent(cmd="log", data="Exception in thread "+threading.currentThread().getName()))
615 wx.PostEvent(self, XmlServerEvent(cmd="logexception", data=exc))
616 else:
617 self.lw.logexception(exc)
618
619 - def excepthook(self, *args):
620 """Replacement exception handler that sends stuff to our log window"""
621 self.LogException(args)
622
624 """Return certificate filename
625
626 By default $HOME (or My Documents) / .bitfling.key
627 but can be overridden with "certificatefile" config key"""
628
629 if guihelper.IsMSWindows():
630
631 from win32com.shell import shell, shellcon
632 path=shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
633 path=os.path.join(path, ".bitfling.key")
634 else:
635 path=os.path.expanduser("~/.bitfling.key")
636 return self.config.Read("certificatefile", path)
637
638
639 - def StartIfICan(self):
640 certfile=self.GetCertificateFilename()
641 if not os.path.isfile(certfile):
642 wx.BeginBusyCursor(wx.StockCursor(wx.CURSOR_ARROWWAIT))
643 bi=wx.BusyInfo("Creating BitFling host certificate and keys")
644 try:
645 generate_certificate(certfile)
646 if not os.path.isfile(certfile):
647
648 certfile=None
649 finally:
650 del bi
651 wx.EndBusyCursor()
652 port=self.config.ReadInt("port", 12652)
653 if port<1 or port>65535: port=None
654 host=self.config.Read("bindaddress", "")
655 if certfile is None or port is None:
656 return
657 self.Log("Starting on port "+`port`)
658 key=paramiko.DSSKey.from_private_key_file(certfile)
659 fp=paramiko.util.hexify(key.get_fingerprint())
660 self.configpanel.fingerprint.SetValue(fp)
661 self.configpanel.porttext.SetLabel(`port`)
662 self.configpanel.GetSizer().Layout()
663 self.xmlrpcserver=BitFlingService(self, host, port, key)
664 self.xmlrpcserver.setDaemon(True)
665 self.xmlrpcserver.start()
666
667
669 key=paramiko.DSSKey.generate()
670 key.write_private_key_file(outfile, None)
671
672
673
675
676 - def __init__(self, mainwin, host, port, servercert):
679
681 wx.PostEvent(self.mainwin, XmlServerEvent(cmd="log", data=msg))
682
684 wx.PostEvent(self.mainwin, XmlServerEvent(cmd="logexception", data=exc))
685
688
689 - def OnNewUser(self, clientaddr, username, password):
691
693 method="exp_"+method
694 if not hasattr(self, method):
695 raise Fault(3, "No such method")
696
697 context={ 'username': username, 'clientaddr': clientaddr }
698
699 return getattr(self, method)(*params, **{'context': context})
700
701
703
704 - def __init__(self, mainwin, host, port, servercert):
707
709 for i in range(10000):
710 if i not in self.handles:
711 self.handles[i]=[context, comm]
712 return i
713
715
716 return self.handles[num][1]
717
718
719
725
728
729 - def exp_deviceopen(self, port, baud, timeout, hardwareflow, softwareflow, context):
733
739
742
745
748
752
754 "Special handling for empty lists and exceptions"
755 try:
756 res=self.gethandle(context, handle).sendatcommand(atcommand.data, ignoreerror=ignoreerror)
757 if len(res)==0:
758 res=1
759 except:
760 res=0
761 return res
762
765
768
773
776
778 theApp=wx.PySimpleApp()
779
780 menu=wx.Menu()
781
782
783
784 menu.Append(ID_EXIT, "Exit")
785
786 mw=MainWindow(None, -1, "BitFling")
787 taskwin=MyTaskBarIcon(mw, menu)
788 mw.taskwin=taskwin
789 theApp.MainLoop()
790
791 if __name__ == '__main__':
792 run()
793