Skip to content

Commit

Permalink
Gaffer : Version the preferences location
Browse files Browse the repository at this point in the history
This avoids problems caused by saving preferences in a later version in a format not compatible with an earlier version. The most common problem is saving a layout containing an editor that doesn't exist in the earlier version, and version 1.4 will shortly be introducing a new editor.

Fixes #368
Fixes #2882
  • Loading branch information
johnhaddon committed Nov 20, 2023
1 parent 582fe39 commit 3b40f72
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 8 deletions.
1 change: 1 addition & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Improvements
------------

- Preferences : The preferences location now includes the Gaffer major version (`$HOME/gaffer/startup/1.4`). Among other things, this avoids problems caused by saving a UI layout in a new version and then running an older version (#368, #2882).
- Toolbars : Changed hotkey behavior to toogle any tool on and off. Exclusive tools such as the Translate and Crop Window tools activate the first tool (currently Selection Tool) when they are toggled off.
- CropWindowTool : Added <kbd>`Alt` + <kbd>`C` for toggling both the crop window tool and the relevant crop window `enabled` plug.
- TaskList, FrameMask : Reimplemented in C++ for improved performance.
Expand Down
48 changes: 48 additions & 0 deletions bin/__gaffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@
# gaffer should use the gaffer wrapper script (also in this directory)
# as it ensures the correct process environment is set up prior to launch.

import contextlib
import os
import pathlib
import shutil
import sys
import signal
import tempfile
import warnings

# Get rid of the annoying signal handler which turns Ctrl-C into a KeyboardInterrupt exception
Expand All @@ -65,6 +69,50 @@
from Gaffer._Gaffer import _nameProcess
_nameProcess()

# Add version-specific startup location to `GAFFER_STARTUP_PATHS`.

from Gaffer.About import About

startupDir = pathlib.Path( os.getenv( "HOME" ) ) / "gaffer" / "startup"
versionedStartupDir = startupDir / "{}.{}".format( About.milestoneVersion(), About.majorVersion() )

if not versionedStartupDir.exists() and startupDir.is_dir() :

# Version-specific directory doesn't exist. Find the
# latest available version.
maxVersion = None
for candidate in startupDir.iterdir() :
try :
version = [ int( x ) for x in candidate.name.split( "." ) ]
assert( len( version ) == 2 )
except :
continue
if maxVersion is None or version > maxVersion :
maxVersion = version

# Copy from lastest available version. We first copy recursively
# to a temporary directory and then rename that atomically to avoid
# conflicts from concurrently running processes.
sourceDir = startupDir if maxVersion is None else startupDir / "{}.{}".format( *maxVersion )

tempDir = pathlib.Path( tempfile.mkdtemp( dir = startupDir, prefix = "__migrating" ) )
for source in sourceDir.iterdir() :
if not source.name.startswith( "__migrating" ) :
if source.is_file( ) :
shutil.copyfile( source, tempDir / source.name )
else :
shutil.copytree( source, tempDir / source.name )

# Ignore FileExistsError, as another process may have beaten us to it.
with contextlib.suppress( FileExistsError ) :
tempDir.rename( versionedStartupDir )
IECore.msg( IECore.Msg.Level.Info, "Gaffer", "Migrated preferences from \"{}\"".format( sourceDir ) )

if str( versionedStartupDir ) not in os.environ["GAFFER_STARTUP_PATHS"].split( os.pathsep ) :
os.environ["GAFFER_STARTUP_PATHS"] = os.pathsep.join( [ str( versionedStartupDir ), os.environ["GAFFER_STARTUP_PATHS"] ] )

# Load and run application.

helpText = """Usage :
gaffer -help Print this message
Expand Down
1 change: 0 additions & 1 deletion bin/gaffer
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ fi

prependToPath "$HOME/gaffer/apps:$GAFFER_ROOT/apps" GAFFER_APP_PATHS

prependToPath "$HOME/gaffer/startup" GAFFER_STARTUP_PATHS
appendToPath "$GAFFER_ROOT/startup" GAFFER_STARTUP_PATHS

prependToPath "$HOME/gaffer/startup" CORTEX_STARTUP_PATHS
Expand Down
1 change: 0 additions & 1 deletion bin/gaffer.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ set CORTEX_POINTDISTRIBUTION_TILESET=%GAFFER_ROOT%\resources\cortex\tileset_2048

call :prependToPath "%USERPROFILE%\gaffer\apps;%GAFFER_ROOT%\apps" GAFFER_APP_PATHS

call :prependToPath "%USERPROFILE%\gaffer\startup" GAFFER_STARTUP_PATHS
call :appendToPath "%GAFFER_ROOT%\startup" GAFFER_STARTUP_PATHS

call :prependToPath "%GAFFER_ROOT%\graphics" GAFFERUI_IMAGE_PATHS
Expand Down
7 changes: 3 additions & 4 deletions include/Gaffer/ApplicationRoot.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,9 @@ class GAFFER_API ApplicationRoot : public GraphComponent
void savePreferences() const;
/// Saves the current preferences value to the specified file.
virtual void savePreferences( const std::filesystem::path &fileName ) const;
/// Returns ~/gaffer/startup/appName - the directory in which preferences are
/// stored, and ensures that the directory exists. Other application components
/// may use this location to store settings they wish to persist across invocations.
/// \todo Perhaps this should include a major version number in the future.
/// Returns the directory in which application preferences are stored,
/// ensuring that it exists. Other application components may use this
/// location to store settings they wish to persist across invocations.
std::filesystem::path preferencesLocation() const;
//@}

Expand Down
2 changes: 1 addition & 1 deletion python/GafferTest/ApplicationRootTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

class ApplicationRootTest( GafferTest.TestCase ) :

__defaultPreferencesFile = pathlib.Path( "~/gaffer/startup/testApp/preferences.py" ).expanduser()
__defaultPreferencesFile = pathlib.Path( "~/gaffer/startup/{}.{}/testApp/preferences.py".format( Gaffer.About.milestoneVersion(), Gaffer.About.majorVersion() ) ).expanduser()

class testApp( Gaffer.Application ) :

Expand Down
47 changes: 47 additions & 0 deletions python/GafferTest/ApplicationTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
##########################################################################

import os
import shutil
import time
import subprocess
import unittest
Expand All @@ -60,6 +61,52 @@ def testWrapperDoesntDuplicatePaths( self ) :
value = subprocess.check_output( [ str( Gaffer.executablePath() ), "env", "python", "-c", "import os; print(os.environ['{}'])".format( v ) ], universal_newlines = True )
self.assertEqual( value.strip(), os.environ[v] )

def testStartupDirectoryMigration( self ) :

originalHome = os.environ["HOME"]
self.addCleanup( os.environ.__setitem__, "HOME", originalHome )
testHome = self.temporaryDirectory() / "home"
os.environ["HOME"] = str( testHome )

for previousVersions in [
[ None ],
[ "0.9", "1.0", "1.3" ]
] :
with self.subTest( previousVersions = previousVersions ) :

for version in previousVersions :
startupDir = testHome / "gaffer" / "startup"
if version is not None :
startupDir /= version
startupDir.mkdir( parents = True )
with open( startupDir / "test.py", "w" ) as testFile :
testFile.write( "# {}".format( version ) )
startupSubDir = startupDir / "subDir"
startupSubDir.mkdir()
with open( startupSubDir / "test.py", "w" ) as testFile :
testFile.write( "# subdir {}".format( version ) )

processes = []
for i in range( 0, 10 ) :
processes.append(
subprocess.Popen(
[ str( Gaffer.executablePath() ), "license" ],
stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL
)
)
for process in processes :
process.wait()

expectedDir = testHome / "gaffer" / "startup" / "{}.{}".format( Gaffer.About.milestoneVersion(), Gaffer.About.majorVersion() )
self.assertTrue( expectedDir.is_dir() )

with open( expectedDir / "test.py" ) as testFile :
self.assertEqual( testFile.readline(), "# {}".format( previousVersions[-1] ) )
with open( expectedDir / "subDir" / "test.py" ) as testFile :
self.assertEqual( testFile.readline(), "# subdir {}".format( previousVersions[-1] ) )

shutil.rmtree( testHome )

@unittest.skipIf( os.name == "nt", "Process name is not controllable on Windows.")
def testProcessName( self ) :

Expand Down
3 changes: 2 additions & 1 deletion src/Gaffer/ApplicationRoot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include "Gaffer/ApplicationRoot.h"

#include "Gaffer/Preferences.h"
#include "Gaffer/Version.h"

#include <filesystem>

Expand Down Expand Up @@ -130,7 +131,7 @@ std::filesystem::path ApplicationRoot::preferencesLocation() const
}

std::filesystem::path result = home;
result = result / "gaffer" / "startup" / getName().string();
result = result / "gaffer" / "startup" / fmt::format( "{}.{}", GAFFER_MILESTONE_VERSION, GAFFER_MAJOR_VERSION ) / getName().string();

std::filesystem::create_directories( result );

Expand Down

0 comments on commit 3b40f72

Please sign in to comment.