1919import time
2020import traceback
2121import webbrowser
22- import zlib
22+ from pathlib import Path
2323from subprocess import PIPE
2424from subprocess import STDOUT
2525from zipfile import ZipFile
5555is_py3 = (sys .version_info [0 ] == 3 )
5656
5757# @note: @es3n1n: plugin-related stuff
58- VERSION = "1.0"
59- NETNODE_NAME = "$ WakaTime"
60-
61- # @note: @es3n1n: netnode-related stuff
62- BLOB_SIZE = 1024
63- STR_KEYS_TAG = 'N'
64- STR_TO_INT_MAP_TAG = 'O'
58+ VERSION = "1.1"
6559
6660# @note: @es3n1n: ida-related stuff
6761ida_ver = idaapi .get_kernel_version ()
10397SEND_BUFFER_SECONDS = 30 # seconds between sending buffered heartbeats to API
10498
10599
106- # @credits: https://github.com/williballenthin/ida-netnode
107- class NetnodeCorruptError (RuntimeError ):
108- pass
109-
110-
111- class Netnode (object ):
112- def __init__ (self , netnode_name ):
113- self ._netnode_name = netnode_name
114- # self._n = idaapi.netnode(netnode_name, namelen=0, do_create=True)
115- self ._n = idaapi .netnode (netnode_name , 0 , True )
116-
117- @staticmethod
118- def _decompress (data ):
119- """
120- args:
121- data (bytes): the data to decompress
122- returns:
123- bytes: the decompressed data.
124- """
125- return zlib .decompress (data )
126-
127- @staticmethod
128- def _compress (data ):
129- """
130- args:
131- data (bytes): the data to compress
132- returns:
133- bytes: the compressed data.
134- """
135- return zlib .compress (data )
136-
137- @staticmethod
138- def _encode (data ):
139- """
140- args:
141- data (object): the data to serialize to json.
142- returns:
143- bytes: the ascii-encoded serialized data buffer.
144- """
145- return json .dumps (data ).encode ("ascii" )
146-
147- @staticmethod
148- def _decode (data ):
149- """
150- args:
151- data (bytes): the ascii-encoded json serialized data buffer.
152- returns:
153- object: the deserialized object.
154- """
155- return json .loads (data .decode ("ascii" ))
156-
157- def _get_next_slot (self , tag ):
158- """
159- get the first unused supval table key, or 0 if the
160- table is empty.
161- useful for filling the supval table sequentially.
162- """
163- slot = self ._n .suplast (tag )
164- if slot is None or slot == idaapi .BADNODE :
165- return 0
166- else :
167- return slot + 1
168-
169- def _strdel (self , key ):
170- assert isinstance (key , str )
171-
172- did_del = False
173- storekey = self ._n .hashval (key , STR_TO_INT_MAP_TAG )
174- if storekey is not None :
175- storekey = int (storekey .decode ('utf-8' ))
176- self ._n .delblob (storekey , STR_KEYS_TAG )
177- self ._n .hashdel (key , STR_TO_INT_MAP_TAG )
178- did_del = True
179- if self ._n .hashval (key ):
180- self ._n .hashdel (key )
181- did_del = True
182-
183- if not did_del :
184- raise KeyError ("'{}' not found" .format (key ))
185-
186- def _strset (self , key , value ):
187- assert isinstance (key , str )
188- assert value is not None
189-
190- try :
191- self ._strdel (key )
192- except KeyError :
193- pass
194-
195- if len (value ) > BLOB_SIZE :
196- storekey = self ._get_next_slot (STR_KEYS_TAG )
197- self ._n .setblob (value , storekey , STR_KEYS_TAG )
198- self ._n .hashset (key , str (storekey ).encode ('utf-8' ),
199- STR_TO_INT_MAP_TAG )
200- else :
201- self ._n .hashset (key , bytes (value ))
202-
203- def _strget (self , key ):
204- assert isinstance (key , str )
205-
206- storekey = self ._n .hashval (key , STR_TO_INT_MAP_TAG )
207- if storekey is not None :
208- storekey = int (storekey .decode ('utf-8' ))
209- v = self ._n .getblob (storekey , STR_KEYS_TAG )
210- if v is None :
211- raise NetnodeCorruptError ()
212- return v
213-
214- v = self ._n .hashval (key )
215- if v is not None :
216- return v
217-
218- raise KeyError ("'{}' not found" .format (key ))
219-
220- def __getitem__ (self , key ):
221- if isinstance (key , str ):
222- v = self ._strget (key )
223- else :
224- raise TypeError ("cannot use {} as key" .format (type (key )))
225-
226- data = self ._decompress (v )
227- return self ._decode (data )
228-
229- def __setitem__ (self , key , value ):
230- """
231- does not support setting a value to None.
232- value must be json-serializable.
233- key must be a string or integer.
234- """
235- assert value is not None
236-
237- v = self ._compress (self ._encode (value ))
238- if isinstance (key , str ):
239- self ._strset (key , v )
240- else :
241- raise TypeError ("cannot use {} as key" .format (type (key )))
242-
243- def __delitem__ (self , key ):
244- if isinstance (key , str ):
245- self ._strdel (key )
246- else :
247- raise TypeError ("cannot use {} as key" .format (type (key )))
248-
249- def get (self , key , default = None ):
250- try :
251- return self [key ]
252- except (KeyError , zlib .error ):
253- return default
254-
255- def __contains__ (self , key ):
256- try :
257- if self [key ] is not None :
258- return True
259- return False
260- except (KeyError , zlib .error ):
261- return False
262-
263- def _iter_str_keys_small (self ):
264- # string keys for all small values
265- if using_ida7api :
266- i = self ._n .hashfirst ()
267- else :
268- i = self ._n .hash1st () # noqa
269- while i != idaapi .BADNODE and i is not None :
270- yield i
271- if using_ida7api :
272- i = self ._n .hashnext (i )
273- else :
274- i = self ._n .hashnxt (i ) # noqa
275-
276- def _iter_str_keys_large (self ):
277- # string keys for all big values
278- if using_ida7api :
279- i = self ._n .hashfirst (STR_TO_INT_MAP_TAG )
280- else :
281- i = self ._n .hash1st (STR_TO_INT_MAP_TAG ) # noqa
282- while i != idaapi .BADNODE and i is not None :
283- yield i
284- if using_ida7api :
285- i = self ._n .hashnext (i , STR_TO_INT_MAP_TAG )
286- else :
287- i = self ._n .hashnxt (i , STR_TO_INT_MAP_TAG ) # noqa
288-
289- def iterkeys (self ):
290- for key in self ._iter_str_keys_small ():
291- yield key
292-
293- for key in self ._iter_str_keys_large ():
294- yield key
295-
296- def keys (self ):
297- return [k for k in list (self .iterkeys ())]
298-
299- def itervalues (self ):
300- for k in list (self .keys ()):
301- yield self [k ]
302-
303- def values (self ):
304- return [v for v in list (self .itervalues ())]
305-
306- def iteritems (self ):
307- for k in list (self .keys ()):
308- yield k , self [k ]
309-
310- def items (self ):
311- return [(k , v ) for k , v in list (self .iteritems ())]
312-
313- def kill (self ):
314- self ._n .kill ()
315- self ._n = idaapi .netnode (self ._netnode_name , 0 , True )
316-
317-
318- # @note: @es3n1n: Initializing global netnode for config
319- NETNODE = Netnode (NETNODE_NAME )
320-
321-
322100# @note: @es3n1n: Utils
323101class Popen (subprocess .Popen ):
324102 """Patched Popen to prevent opening cmd window on Windows platform."""
@@ -335,8 +113,42 @@ def __init__(self, *args, **kwargs):
335113 super (Popen , self ).__init__ (* args , ** kwargs )
336114
337115
116+ class Config :
117+ path = Path (__file__ ).parent .resolve ().absolute () / 'wakatime.config.json'
118+ _cached = None
119+
120+ @classmethod
121+ def read (cls ):
122+ if cls ._cached is not None :
123+ return cls ._cached
124+
125+ if not cls .path .exists ():
126+ cls ._cached = dict ()
127+ return cls ._cached
128+
129+ try :
130+ cls ._cached = json .loads (cls .path .read_text ())
131+ except json .JSONDecodeError :
132+ cls ._cached = dict ()
133+ return cls ._cached
134+
135+ @classmethod
136+ def write (cls , value ):
137+ cls .path .write_text (json .dumps (value ))
138+
139+ @classmethod
140+ def get_var (cls , key , default = None ):
141+ return cls .read ().get (key , default )
142+
143+ @classmethod
144+ def set_var (cls , key , value ):
145+ cfg = cls .read ()
146+ cfg [key ] = value
147+ cls .write (cfg )
148+
149+
338150def log (lvl , message , * args , ** kwargs ):
339- if lvl == DEBUG and NETNODE . get ('debug' , 'false' ) == 'false' :
151+ if lvl == DEBUG and Config . get_var ('debug' , 'false' ) == 'false' :
340152 return
341153
342154 msg = message
@@ -695,7 +507,7 @@ def read(self):
695507 if self ._key :
696508 return self ._key
697509
698- key = NETNODE . get ('api_key' )
510+ key = Config . get_var ('api_key' )
699511 if key :
700512 self ._key = key
701513 return self ._key
@@ -714,9 +526,8 @@ def read(self):
714526 return self ._key
715527
716528 def write (self , key ):
717- global NETNODE
718529 self ._key = key
719- NETNODE [ 'api_key' ] = str ( key )
530+ Config . set_var ( 'api_key' , key )
720531
721532
722533APIKEY = ApiKey ()
@@ -989,7 +800,8 @@ def term(self):
989800
990801 @staticmethod
991802 def run (* args ): # noqa
992- dbg = NETNODE .get ('debug' , "false" )
803+ dbg = Config .get_var ('debug' , "false" )
804+
993805 fmt = '''AUTOHIDE NONE
994806WakaTime integration for IDA Pro
995807Plugin version: v{}
@@ -1000,7 +812,7 @@ def run(*args): # noqa
1000812
1001813 if ret == 1 :
1002814 dbg = "false" if dbg == "true" else "true"
1003- NETNODE [ 'debug' ] = dbg
815+ Config . set_var ( 'debug' , dbg )
1004816 log (INFO , 'Set debug to: {}' .format (dbg ))
1005817
1006818 if ret == 0 :
0 commit comments