From e391950390359d991805377ba9684d2f588d20c1 Mon Sep 17 00:00:00 2001 From: Linda Kladivova <49241681+lindakladivova@users.noreply.github.com> Date: Tue, 18 Aug 2020 19:13:32 +0200 Subject: [PATCH] wxGUI: Ask to remove lock when switching to another mapset from datacatalog and menu (#906) --- gui/wxpython/datacatalog/tree.py | 49 ++++++++++---------- gui/wxpython/lmgr/frame.py | 18 +++++++- gui/wxpython/startup/guiutils.py | 76 ++++++++++++++++++++++++++++---- lib/python/grassdb/checks.py | 19 ++++++++ 4 files changed, 127 insertions(+), 35 deletions(-) diff --git a/gui/wxpython/datacatalog/tree.py b/gui/wxpython/datacatalog/tree.py index 7c8aedfbca6..17794e7bd0a 100644 --- a/gui/wxpython/datacatalog/tree.py +++ b/gui/wxpython/datacatalog/tree.py @@ -44,7 +44,8 @@ delete_mapsets_interactively, delete_locations_interactively, download_location_interactively, - delete_grassdb_interactively + delete_grassdb_interactively, + can_switch_mapset_interactive ) from grass.pydispatch.signal import Signal @@ -1259,30 +1260,30 @@ def OnEndDrag(self, node, event): event.Veto() return - def OnSwitchDbLocationMapset(self, event): - """Switch to location and mapset""" - self._SwitchDbLocationMapset() - - def _SwitchDbLocationMapset(self): + def OnSwitchMapset(self, event): """Switch to location and mapset""" genv = gisenv() - # Distinguish when only part of db/location/mapset is changed. - if ( - self.selected_grassdb[0].data['name'] == genv['GISDBASE'] - and self.selected_location[0].data['name'] == genv['LOCATION_NAME'] - ): - self.changeMapset.emit(mapset=self.selected_mapset[0].data['name']) - elif self.selected_grassdb[0].data['name'] == genv['GISDBASE']: - self.changeLocation.emit(mapset=self.selected_mapset[0].data['name'], - location=self.selected_location[0].data['name'], - dbase=None) - else: - self.changeLocation.emit(mapset=self.selected_mapset[0].data['name'], - location=self.selected_location[0].data['name'], - dbase=self.selected_grassdb[0].data['name']) - self.UpdateCurrentDbLocationMapsetNode() - self.ExpandCurrentMapset() - self.RefreshItems() + grassdb = self.selected_grassdb[0].data['name'] + location = self.selected_location[0].data['name'] + mapset = self.selected_mapset[0].data['name'] + + if can_switch_mapset_interactive(self, grassdb, location, mapset): + # Switch to mapset in the same location + if (grassdb == genv['GISDBASE'] and location == genv['LOCATION_NAME']): + self.changeMapset.emit(mapset=mapset) + # Switch to mapset in the same grassdb + elif grassdb == genv['GISDBASE']: + self.changeLocation.emit(mapset=mapset, + location=location, + dbase=None) + # Switch to mapset in a different grassdb + else: + self.changeLocation.emit(mapset=mapset, + location=location, + dbase=grassdb) + self.UpdateCurrentDbLocationMapsetNode() + self.ExpandCurrentMapset() + self.RefreshItems() def OnMetadata(self, event): """Show metadata of any raster/vector/3draster""" @@ -1452,7 +1453,7 @@ def _popupMenuMapset(self): item = wx.MenuItem(menu, wx.ID_ANY, _("&Switch mapset")) menu.AppendItem(item) - self.Bind(wx.EVT_MENU, self.OnSwitchDbLocationMapset, item) + self.Bind(wx.EVT_MENU, self.OnSwitchMapset, item) if ( self.selected_grassdb[0].data['name'] == genv['GISDBASE'] and self.selected_location[0].data['name'] == genv['LOCATION_NAME'] diff --git a/gui/wxpython/lmgr/frame.py b/gui/wxpython/lmgr/frame.py index 86c2be1e4f1..09d1c80a08a 100644 --- a/gui/wxpython/lmgr/frame.py +++ b/gui/wxpython/lmgr/frame.py @@ -43,6 +43,9 @@ from grass.script import core as grass from grass.script.utils import decode +from startup.guiutils import ( + can_switch_mapset_interactive +) from core.gcmd import RunCommand, GError, GMessage from core.settings import UserSettings, GetDisplayVectSettings @@ -1062,6 +1065,8 @@ def OnRunScript(self, event): def OnChangeLocation(self, event): """Change current location""" dlg = LocationDialog(parent=self) + gisenv = grass.gisenv() + if dlg.ShowModal() == wx.ID_OK: location, mapset = dlg.GetValues() dlg.Destroy() @@ -1072,7 +1077,11 @@ def OnChangeLocation(self, event): message=_( "No location/mapset provided. Operation canceled.")) return # this should not happen - self.ChangeLocation(location, mapset) + if can_switch_mapset_interactive(self, + gisenv['GISDBASE'], + location, + mapset): + self.ChangeLocation(location, mapset) def ChangeLocation(self, location, mapset, dbase=None): if dbase: @@ -1132,6 +1141,7 @@ def OnCreateMapset(self, event): def OnChangeMapset(self, event): """Change current mapset""" dlg = MapsetDialog(parent=self) + gisenv = grass.gisenv() if dlg.ShowModal() == wx.ID_OK: mapset = dlg.GetMapset() @@ -1141,7 +1151,11 @@ def OnChangeMapset(self, event): GError(parent=self, message=_("No mapset provided. Operation canceled.")) return - self.ChangeMapset(mapset) + if can_switch_mapset_interactive(self, + gisenv['GISDBASE'], + gisenv['LOCATION_NAME'], + mapset): + self.ChangeMapset(mapset) def ChangeMapset(self, mapset): """Change current mapset and update map display title""" diff --git a/gui/wxpython/startup/guiutils.py b/gui/wxpython/startup/guiutils.py index 21b088f7aa2..f38b037d6d3 100644 --- a/gui/wxpython/startup/guiutils.py +++ b/gui/wxpython/startup/guiutils.py @@ -21,7 +21,11 @@ import grass.script as gs from grass.script import gisenv -from grass.grassdb.checks import mapset_exists, location_exists +from grass.grassdb.checks import ( + mapset_exists, + location_exists, + is_mapset_locked, + get_mapset_lock_info) from grass.grassdb.create import create_mapset, get_default_mapset_name from grass.grassdb.manage import ( delete_mapset, @@ -646,14 +650,14 @@ def delete_grassdb_interactively(guiparent, grassdb): if issue: dlg = wx.MessageDialog( - parent=guiparent, - message=_( - "Cannot delete GRASS database from disk for the following reason:\n\n" - "{}\n\n" - "GRASS database will not be deleted." - ).format(issue), - caption=_("Unable to delete selected GRASS database"), - style=wx.OK | wx.ICON_WARNING + parent=guiparent, + message=_( + "Cannot delete GRASS database from disk for the following reason:\n\n" + "{}\n\n" + "GRASS database will not be deleted." + ).format(issue), + caption=_("Unable to delete selected GRASS database"), + style=wx.OK | wx.ICON_WARNING ) dlg.ShowModal() else: @@ -692,6 +696,60 @@ def delete_grassdb_interactively(guiparent, grassdb): return deleted +def can_switch_mapset_interactive(guiparent, grassdb, location, mapset): + """ + Checks if mapset is locked and offers to remove the lock file. + + Returns True if user wants to switch to the selected mapset in spite of + removing lock. Returns False if a user wants to stay in the current + mapset or if an error was encountered. + """ + can_switch = True + mapset_path = os.path.join(grassdb, location, mapset) + + if is_mapset_locked(mapset_path): + info = get_mapset_lock_info(mapset_path) + user = info['owner'] if info['owner'] else _('unknown') + lockpath = info['lockpath'] + timestamp = info['timestamp'] + + dlg = wx.MessageDialog( + parent=guiparent, + message=_("User {user} is already running GRASS in selected mapset " + "<{mapset}>\n (file {lockpath} created {timestamp} " + "found).\n\n" + "Concurrent use not allowed.\n\n" + "Do you want to stay in the current mapset or remove " + ".gislock and switch to selected mapset?" + ).format(user=user, + mapset=mapset, + lockpath=lockpath, + timestamp=timestamp), + caption=_("Mapset is in use"), + style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION, + ) + dlg.SetYesNoLabels("S&witch to selected mapset", + "S&tay in current mapset") + if dlg.ShowModal() == wx.ID_YES: + # Remove lockfile + try: + os.remove(lockpath) + except IOError as e: + wx.MessageBox( + parent=guiparent, + caption=_("Error when removing lock file"), + message=_("Unable to remove {lockpath}.\n\n Details: {error}." + ).format(lockpath=lockpath, + error=e), + style=wx.OK | wx.ICON_ERROR | wx.CENTRE + ) + can_switch = False + else: + can_switch = False + dlg.Destroy() + return can_switch + + def import_file(guiparent, filePath): """Tries to import file as vector or raster. diff --git a/lib/python/grassdb/checks.py b/lib/python/grassdb/checks.py index aad30528ced..e44cf8ed1ee 100644 --- a/lib/python/grassdb/checks.py +++ b/lib/python/grassdb/checks.py @@ -11,6 +11,7 @@ import os +import datetime from pathlib import Path @@ -106,6 +107,24 @@ def get_lockfile_if_present(database, location, mapset): return None +def get_mapset_lock_info(mapset_path): + """Get information about .gislock file. + Assumes lock file exists, use is_mapset_locked to find out. + Returns information as a dictionary with keys + 'owner' (None if unknown), 'lockpath', and 'timestamp'. + """ + info = {} + lock_name = ".gislock" + info['lockpath'] = os.path.join(mapset_path, lock_name) + try: + info['owner'] = Path(info['lockpath']).owner() + except KeyError: + info['owner'] = None + info['timestamp'] = (datetime.datetime.fromtimestamp( + os.path.getmtime(info['lockpath']))).replace(microsecond=0) + return info + + def can_start_in_mapset(mapset_path, ignore_lock=False): """Check if a mapset from a gisrc file is usable for new session""" if not is_mapset_valid(mapset_path):