Skip to content

Commit 7612baa

Browse files
Session object (#285)
* Session Object * Remove unused bookmarks dictionary and update session data structure to include process name * change behavior based on feedback - Saving now always allowed - moved Version functions into session - renamed Session to session to follow naming conventions * Implement symbol resolution for loaded bookmarks * add bookmark resolver by process region Also add index to memory_region table * remove symbol resolve --> only using region resolving * Add TODO for a potential symbol resolution --------- Co-authored-by: Korcan Karaokçu <[email protected]>
1 parent 5349b4a commit 7612baa

File tree

16 files changed

+1125
-506
lines changed

16 files changed

+1125
-506
lines changed

GUI/MainWindow.py

Lines changed: 97 additions & 87 deletions
Large diffs are not rendered by default.

GUI/MainWindow.ui

Lines changed: 218 additions & 191 deletions
Large diffs are not rendered by default.

GUI/Session/session.py

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
import os
2+
from enum import IntFlag, auto
3+
4+
from PyQt6.QtCore import QObject
5+
from PyQt6.QtGui import QCloseEvent
6+
from PyQt6.QtWidgets import QFileDialog, QMessageBox
7+
8+
from GUI.States import states
9+
from libpince import utils, debugcore
10+
from tr.tr import TranslationConstants as tr
11+
12+
13+
class SessionDataChanged(IntFlag):
14+
NONE = auto()
15+
ADDRESS_TREE = auto()
16+
BOOKMARKS = auto()
17+
NOTES = auto()
18+
PROCESS_NAME = auto()
19+
20+
21+
def migrate_version(content: any) -> dict[str, any]:
22+
if not hasattr(content, "version") and type(content) == list:
23+
return legacy_to_v1(content)
24+
25+
return content
26+
27+
28+
def is_valid_session_data(content: dict[str, any]) -> bool:
29+
keys = ["version", "notes", "bookmarks", "address_tree", "process_name"]
30+
for key in keys:
31+
if key not in content:
32+
return False
33+
34+
return True
35+
36+
37+
def legacy_to_v1(content: list) -> dict[str, any]:
38+
print("Migrating legacy session data to version 1")
39+
return {"version": 1, "notes": "", "bookmarks": {}, "address_tree": content, "process_name": ""}
40+
41+
42+
class Session:
43+
def __init__(self) -> None:
44+
# Anything labled with pct should be saved to the session file
45+
self.pct_notes: str = ""
46+
self.pct_bookmarks: dict[int, dict] = {}
47+
self.pct_version: int = 1
48+
self.pct_address_tree: list = []
49+
self.pct_process_name: str = ""
50+
self.data_changed = SessionDataChanged.NONE
51+
self.file_path: str = os.curdir
52+
self.last_file_name: str = "" # process name or file name
53+
54+
def save_session(self) -> bool:
55+
"""
56+
Save the current session to a file.
57+
58+
Args:
59+
None
60+
Returns:
61+
bool: True if the session was saved successfully, False otherwise.
62+
"""
63+
64+
file_path, _ = QFileDialog.getSaveFileName(
65+
None, tr.SAVE_PCT_FILE, self.file_path + "/" + self.last_file_name, tr.FILE_TYPES_PCT
66+
)
67+
if not file_path:
68+
return False
69+
70+
# until address tree is model view and properly read from this new session object,
71+
# address tree must save its data to the session object via signal
72+
if self.data_changed & SessionDataChanged.ADDRESS_TREE:
73+
states.session_signals.on_save.emit()
74+
75+
session = {
76+
"version": self.pct_version,
77+
"notes": self.pct_notes,
78+
"bookmarks": self.pct_bookmarks,
79+
"address_tree": self.pct_address_tree,
80+
"process_name": self.pct_process_name,
81+
}
82+
83+
file_path = utils.append_file_extension(file_path, "pct")
84+
if not utils.save_file(session, file_path):
85+
QMessageBox.information(None, tr.ERROR, tr.FILE_SAVE_ERROR)
86+
return False
87+
88+
self.file_path = os.path.dirname(file_path)
89+
self.last_file_name = os.path.basename(file_path)
90+
self.data_changed = SessionDataChanged.NONE
91+
return True
92+
93+
def check_unsaved_changes(self) -> QMessageBox.StandardButton:
94+
if self.data_changed == SessionDataChanged.NONE:
95+
return QMessageBox.StandardButton.No
96+
97+
unsaved_changes_result = QMessageBox.question(
98+
None,
99+
tr.SAVE_SESSION_QUESTION_TITLE,
100+
tr.SAVE_SESSION_QUESTION_PROMPT,
101+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel,
102+
)
103+
return unsaved_changes_result
104+
105+
def load_session(self) -> bool:
106+
"""
107+
Load a pct session file. Will check for unsaved changes and prompt
108+
the user to save them before loading a new session.
109+
If the user chooses to cancel, the function will return False.
110+
Will also attempt to migrate the session data to the latest version.
111+
112+
Args:
113+
None
114+
Returns:
115+
bool: True if the session was loaded successfully, False otherwise.
116+
117+
"""
118+
119+
unsaved_changes_result = self.check_unsaved_changes()
120+
if unsaved_changes_result == QMessageBox.StandardButton.Cancel:
121+
return False
122+
elif unsaved_changes_result == QMessageBox.StandardButton.Yes:
123+
if not self.save_session():
124+
return False
125+
126+
file_path, _ = QFileDialog.getOpenFileName(
127+
None, tr.OPEN_PCT_FILE, self.file_path + "/" + self.last_file_name, tr.FILE_TYPES_PCT
128+
)
129+
if not file_path:
130+
return False
131+
132+
content = utils.load_file(file_path)
133+
if content is None:
134+
QMessageBox.information(None, tr.ERROR, tr.FILE_LOAD_ERROR.format(file_path))
135+
return False
136+
content = migrate_version(content)
137+
if not is_valid_session_data(content):
138+
QMessageBox.information(None, tr.ERROR, tr.FILE_LOAD_ERROR.format(file_path))
139+
return False
140+
141+
self.pct_version = content["version"]
142+
self.pct_notes = content["notes"]
143+
144+
# Load bookmarks with symbol resolution
145+
self.pct_bookmarks = content["bookmarks"]
146+
self.recalculate_bookmarks()
147+
148+
self.pct_address_tree = content["address_tree"]
149+
150+
self.file_path = os.path.dirname(file_path)
151+
self.last_file_name = os.path.basename(file_path)
152+
153+
states.session_signals.on_load.emit()
154+
self.data_changed = SessionDataChanged.NONE
155+
return True
156+
157+
def pre_exit(self, close_event: QCloseEvent) -> None:
158+
"""
159+
Event handler for the close event of the application.
160+
If there are unsaved changes, prompt the user to save them.
161+
Accepts or ignores the close event based on the user's choice.
162+
163+
Args:
164+
close_event (QCloseEvent): The close event to be handled.
165+
Returns:
166+
None
167+
"""
168+
169+
if self.data_changed == SessionDataChanged.NONE:
170+
close_event.accept()
171+
return
172+
173+
pre_exit_unsaved_changes_result = self.check_unsaved_changes()
174+
if pre_exit_unsaved_changes_result == QMessageBox.StandardButton.Yes:
175+
self.save_session()
176+
close_event.accept()
177+
178+
elif pre_exit_unsaved_changes_result == QMessageBox.StandardButton.Cancel:
179+
close_event.ignore()
180+
else:
181+
close_event.accept()
182+
183+
def recalculate_bookmarks(self):
184+
"""
185+
Recalculate all bookmarks to update their addresses based on their symbols or region info.
186+
This is useful when the process has changed and the addresses may have shifted.
187+
Will not mark session as changed.
188+
189+
Args:
190+
None
191+
Returns:
192+
None
193+
"""
194+
195+
region_dict = utils.get_region_dict(debugcore.currentpid)
196+
197+
new_bookmarks: dict[int, dict] = {}
198+
for addr, value in self.pct_bookmarks.items():
199+
comment = value["comment"]
200+
symbol = value["symbol"]
201+
address_region_details = value["address_region_details"]
202+
new_addr = addr
203+
204+
# We used to resolve the symbol first but parentheses in symbols causes the functions to be called
205+
# For instance, main() won't be resolved to main but rather the function main() will be called
206+
# Because of this behavior, we rely on resolving via regions
207+
# Resolving via regions is more reliable unless the binary has been changed
208+
# TODO: Resolving via symbols could be re-addressed if this behavior were fixed
209+
210+
# resolve via region details
211+
region_name, offset, region_index = address_region_details.values()
212+
region = region_dict.get(region_name, None)
213+
if region is not None:
214+
new_addr = int(region[region_index], 16) + int(offset, 16)
215+
else:
216+
print("[WARN] BookmarkResolver: Could not find region with name:", region_name)
217+
continue
218+
219+
new_bookmarks[new_addr] = {
220+
"symbol": symbol,
221+
"comment": comment,
222+
"address_region_details": address_region_details,
223+
}
224+
225+
self.pct_bookmarks = new_bookmarks
226+
227+
228+
class SessionManager:
229+
session = Session()
230+
231+
@staticmethod
232+
def get_session() -> Session:
233+
return SessionManager.session
234+
235+
@staticmethod
236+
def reset_session() -> None:
237+
session = SessionManager.get_session()
238+
# User has one last chance to save the session before resetting
239+
# result is ignored, because the session is going to be reset anyway
240+
session.check_unsaved_changes()
241+
SessionManager.session = Session()
242+
states.session_signals.new_session.emit()
243+
# Reset the session data changed flag
244+
session.data_changed = SessionDataChanged.NONE
245+
246+
@staticmethod
247+
def save_session() -> None:
248+
SessionManager.get_session().save_session()
249+
250+
@staticmethod
251+
def load_session() -> None:
252+
SessionManager.get_session().load_session()
253+
254+
@staticmethod
255+
def on_process_changed() -> None:
256+
if debugcore.currentpid == -1:
257+
return
258+
259+
if states.exiting:
260+
return
261+
262+
process_name = utils.get_process_name(debugcore.currentpid)
263+
session = SessionManager.get_session()
264+
265+
if session.pct_process_name == process_name:
266+
# process is the same as last one, probably process restarted / reattached
267+
session.recalculate_bookmarks()
268+
return
269+
270+
if session.pct_process_name == "":
271+
# silently set the process name and file name if necessary
272+
session.pct_process_name = process_name
273+
if session.last_file_name == "":
274+
session.last_file_name = utils.append_file_extension(process_name, "pct")
275+
return
276+
277+
if session.pct_process_name != process_name:
278+
# Ask if the user wants to keep the session
279+
keep_session_result = QMessageBox.question(
280+
None,
281+
tr.SESSION_PROCESS_CHANGED_TITLE,
282+
tr.SESSION_PROCESS_CHANGED_PROMPT,
283+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
284+
)
285+
if keep_session_result == QMessageBox.StandardButton.Yes:
286+
session.pct_process_name = process_name
287+
else:
288+
SessionManager.reset_session()
289+
session.pct_process_name = process_name
290+
session.last_file_name = utils.append_file_extension(process_name, "pct")

GUI/States/states.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
process_signals = guitypedefs.ProcessSignals()
55
setting_signals = guitypedefs.SettingSignals()
6+
session_signals = guitypedefs.SessionSignals()
67

78
status_thread = guitypedefs.CheckInferiorStatus()
89
status_thread.start()
@@ -19,10 +20,6 @@
1920
# Currently only used in address_table_loop
2021
exp_cache: dict[str, str] = {}
2122

22-
# Used by tableWidget_Disassemble in MemoryViewer
23-
# Format -> {bookmark_address:comment}
24-
bookmarks: dict[int, str] = {}
25-
2623
# Set to True when app is about to exit
2724
exiting = False
2825

GUI/Utils/guitypedefs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ class ProcessSignals(QObject):
2525
exit = pyqtSignal()
2626

2727

28+
class SessionSignals(QObject):
29+
new_session = pyqtSignal()
30+
on_save = pyqtSignal()
31+
on_load = pyqtSignal()
32+
33+
2834
class Worker(QRunnable):
2935
def __init__(self, fn, *args, **kwargs):
3036
super().__init__()

GUI/Widgets/Bookmark/Bookmark.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
from PyQt6.QtWidgets import QWidget, QMessageBox, QMenu, QListWidgetItem
21
from PyQt6.QtCore import Qt, pyqtSignal
3-
from PyQt6.QtGui import QShortcut, QKeySequence
2+
from PyQt6.QtGui import QKeySequence, QShortcut
3+
from PyQt6.QtWidgets import QListWidgetItem, QMenu, QMessageBox, QWidget
4+
5+
from GUI.Session.session import SessionDataChanged, SessionManager
46
from GUI.States import states
57
from GUI.Utils import guiutils, utilwidgets
68
from GUI.Widgets.Bookmark.Form.BookmarkWidget import Ui_Form
7-
from tr.tr import TranslationConstants as tr
89
from libpince import debugcore, utils
10+
from tr.tr import TranslationConstants as tr
911

1012

1113
# This widget is too intertwined with MemoryViewer, it will be need to be reworked if it gets used in anywhere else
@@ -26,12 +28,14 @@ def __init__(self, parent):
2628
self.shortcut_delete.activated.connect(self.delete_record)
2729
self.shortcut_refresh = QShortcut(QKeySequence("R"), self)
2830
self.shortcut_refresh.activated.connect(self.refresh_table)
31+
self.session = SessionManager.get_session()
2932
self.refresh_table()
33+
states.session_signals.new_session.connect(self.on_new_session)
3034
guiutils.center_to_parent(self)
3135

3236
def refresh_table(self):
3337
self.listWidget.clear()
34-
address_list = [hex(address) for address in states.bookmarks.keys()]
38+
address_list = [hex(address) for address in self.session.pct_bookmarks.keys()]
3539
if debugcore.currentpid == -1:
3640
self.listWidget.addItems(address_list)
3741
else:
@@ -45,7 +49,7 @@ def change_display(self, row):
4549
self.lineEdit_Info.clear()
4650
else:
4751
self.lineEdit_Info.setText(debugcore.get_address_info(current_address))
48-
self.lineEdit_Comment.setText(states.bookmarks[int(current_address, 16)])
52+
self.lineEdit_Comment.setText(self.session.pct_bookmarks[int(current_address, 16)]["comment"])
4953

5054
def listWidget_item_double_clicked(self, item: QListWidgetItem):
5155
self.double_clicked.emit(utils.extract_address(item.text()))
@@ -60,16 +64,18 @@ def exec_add_entry_dialog(self):
6064
return
6165
self.bookmarked.emit(int(address, 16))
6266
self.refresh_table()
67+
self.session.data_changed |= SessionDataChanged.BOOKMARKS
6368

6469
def exec_change_comment_dialog(self, current_address):
6570
self.comment_changed.emit(current_address)
6671
self.refresh_table()
72+
self.session.data_changed |= SessionDataChanged.BOOKMARKS
6773

6874
def listWidget_context_menu_event(self, event):
6975
current_item = guiutils.get_current_item(self.listWidget)
7076
if current_item:
7177
current_address = int(utils.extract_address(current_item.text()), 16)
72-
if current_address not in states.bookmarks:
78+
if current_address not in self.session.pct_bookmarks:
7379
QMessageBox.information(self, tr.ERROR, tr.INVALID_ENTRY)
7480
self.refresh_table()
7581
return
@@ -104,3 +110,8 @@ def delete_record(self):
104110
current_address = int(utils.extract_address(current_item.text()), 16)
105111
self.deleted.emit(current_address)
106112
self.refresh_table()
113+
self.session.data_changed |= SessionDataChanged.BOOKMARKS
114+
115+
def on_new_session(self):
116+
self.session = SessionManager.get_session()
117+
self.refresh_table()

0 commit comments

Comments
 (0)