@@ -219,7 +219,9 @@ private void RunMigrations()
219219 var newConfigFile = GetConfigPath ( CFG_FILE_NAME ) ;
220220 if ( File . Exists ( newConfigFile ) )
221221 {
222- var existingConfig = JsonConvert . DeserializeObject < JObject > ( File . ReadAllText ( newConfigFile ! ) ) ;
222+ var existingContent = LockedRead ( newConfigFile ! ) ;
223+
224+ var existingConfig = JsonConvert . DeserializeObject < JObject > ( existingContent ) ;
223225
224226 var existingConfigVersion = GetConfig ( CFG_JSON_FIELD_CLI_VERSION , "0.0.123" , existingConfig ) ;
225227 var existingPackageVersion = PackageVersion . FromSemanticVersionString ( existingConfigVersion ) ;
@@ -254,7 +256,7 @@ private void RunMigrations()
254256 if ( File . Exists ( oldConfigFile ) ) File . Move ( oldConfigFile , newConfigFile ! ) ;
255257 else return ;
256258
257- var newConfig = JsonConvert . DeserializeObject < JObject > ( File . ReadAllText ( newConfigFile ! ) ) ;
259+ var newConfig = JsonConvert . DeserializeObject < JObject > ( LockedRead ( newConfigFile ! ) ) ;
258260
259261 // Check for additional projects file and ignored directory files and move their data over.
260262 {
@@ -818,8 +820,114 @@ public void WriteConfig(Action<JObject> modifier, bool isOverride = false)
818820 : ConfigDirectoryPath ;
819821
820822 ReadConfigFile ( configFolder , true , false , out var config ) ;
823+ var original = ( JObject ) config . DeepClone ( ) ;
821824 modifier ( config ) ;
822- FlushConfig ( config , configFolder , ! isOverride ) ;
825+
826+ var patch = DiffJObject ( original , config ) ;
827+
828+ ReadConfigFile ( configFolder , true , false , out var latestConfig ) ;
829+ ApplyDiff ( latestConfig , patch ) ;
830+ FlushConfig ( latestConfig , configFolder , ! isOverride ) ;
831+
832+
833+ // chat-gippity wrote these methods...
834+ JObject DiffJObject ( JObject original , JObject modified )
835+ {
836+ var set = new JObject ( ) ;
837+ var remove = new JArray ( ) ;
838+ var children = new JObject ( ) ;
839+
840+ // Detect removed and changed properties
841+ foreach ( var prop in original . Properties ( ) )
842+ {
843+ var name = prop . Name ;
844+
845+ if ( ! modified . TryGetValue ( name , out var newValue ) )
846+ {
847+ remove . Add ( name ) ;
848+ continue ;
849+ }
850+
851+ var oldValue = prop . Value ;
852+
853+ if ( oldValue . Type == JTokenType . Object &&
854+ newValue . Type == JTokenType . Object )
855+ {
856+ var childDiff = DiffJObject ( ( JObject ) oldValue , ( JObject ) newValue ) ;
857+ if ( childDiff . HasValues )
858+ {
859+ children [ name ] = childDiff ;
860+ }
861+ }
862+ else if ( ! JToken . DeepEquals ( oldValue , newValue ) )
863+ {
864+ set [ name ] = newValue . DeepClone ( ) ;
865+ }
866+ }
867+
868+ // Detect added properties
869+ foreach ( var prop in modified . Properties ( ) )
870+ {
871+ if ( ! original . ContainsKey ( prop . Name ) )
872+ {
873+ set [ prop . Name ] = prop . Value . DeepClone ( ) ;
874+ }
875+ }
876+
877+ var diff = new JObject ( ) ;
878+
879+ if ( set . HasValues ) diff [ "$set" ] = set ;
880+ if ( remove . HasValues ) diff [ "$remove" ] = remove ;
881+ if ( children . HasValues ) diff [ "$children" ] = children ;
882+
883+ return diff ;
884+ }
885+ void ApplyDiff ( JObject target , JObject diff )
886+ {
887+ if ( diff == null || ! diff . HasValues )
888+ return ;
889+
890+ // Apply removals
891+ if ( diff [ "$remove" ] is JArray removeArray )
892+ {
893+ foreach ( var item in removeArray )
894+ {
895+ target . Remove ( item . ToString ( ) ) ;
896+ }
897+ }
898+
899+ // Apply sets
900+ if ( diff [ "$set" ] is JObject setObj )
901+ {
902+ foreach ( var prop in setObj . Properties ( ) )
903+ {
904+ target [ prop . Name ] = prop . Value . DeepClone ( ) ;
905+ }
906+ }
907+
908+ // Apply children recursively
909+ if ( diff [ "$children" ] is JObject childrenObj )
910+ {
911+ foreach ( var childProp in childrenObj . Properties ( ) )
912+ {
913+ var childName = childProp . Name ;
914+ var childDiff = ( JObject ) childProp . Value ;
915+
916+ if ( target [ childName ] is JObject childTarget )
917+ {
918+ ApplyDiff ( childTarget , childDiff ) ;
919+ }
920+ else
921+ {
922+ // If missing or not an object, create a new object
923+ var newChild = new JObject ( ) ;
924+ ApplyDiff ( newChild , childDiff ) ;
925+ target [ childName ] = newChild ;
926+ }
927+ }
928+ }
929+ }
930+
823931
824932 }
825933 public T WriteConfig < T > ( string path , [ CanBeNull ] T newValue , bool isOverride = false )
@@ -1087,7 +1195,7 @@ private void FlushConfig(JObject config, string path, bool createSubDirs)
10871195
10881196 try
10891197 {
1090- File . WriteAllText ( fullPath , json ) ;
1198+ LockedWrite ( fullPath , json ) ;
10911199 written = true ;
10921200 }
10931201 catch ( IOException e )
@@ -1495,6 +1603,41 @@ public static bool TryToFindBeamableFolder(string relativePath, out string resul
14951603 return false ;
14961604 }
14971605
1606+ public static void LockedWrite ( string path , string content , int allowedAttempts = 10 , int retryDelayMs = 25 )
1607+ {
1608+ for ( var i = 0 ; i < allowedAttempts ; i ++ )
1609+ {
1610+ try
1611+ {
1612+ using var stream = new FileStream ( path , FileMode . Create , FileAccess . Write , FileShare . None ) ;
1613+ using var writer = new StreamWriter ( stream ) ;
1614+ writer . Write ( content ) ;
1615+ writer . Flush ( ) ;
1616+ }
1617+ catch ( IOException ) when ( i < allowedAttempts )
1618+ {
1619+ Thread . Sleep ( retryDelayMs ) ;
1620+ }
1621+ }
1622+ }
1623+ public static string LockedRead ( string path , int allowedAttempts = 10 , int retryDelayMs = 25 )
1624+ {
1625+ for ( var i = 0 ; i < allowedAttempts ; i ++ )
1626+ {
1627+ try
1628+ {
1629+ using var stream = new FileStream ( path , FileMode . Open , FileAccess . Read , FileShare . Read ) ;
1630+ using var reader = new StreamReader ( stream ) ;
1631+ return reader . ReadToEnd ( ) ;
1632+ }
1633+ catch ( IOException ) when ( i < allowedAttempts )
1634+ {
1635+ Thread . Sleep ( retryDelayMs ) ;
1636+ }
1637+ }
1638+ throw new IOException ( $ "Failed to read file after { allowedAttempts } attempts.") ;
1639+ }
1640+
14981641 /// <summary>
14991642 /// Takes in a folder path and tries to load
15001643 /// </summary>
@@ -1516,23 +1659,33 @@ private static bool ReadConfigFile(string folderPath, bool isOptional, bool enfo
15161659 return false ;
15171660 }
15181661
1519- var content = File . ReadAllText ( fullPath ) ;
1520-
1521- var read = JsonConvert . DeserializeObject < JObject > ( content ) ;
1522- if ( read == null )
1662+ var content = LockedRead ( fullPath ) ;
1663+ // var content = File.ReadAllText(fullPath);
1664+
1665+ try
15231666 {
1524- // do not set the result to a null value, ever.
1525-
1526- if ( isOptional )
1667+ var read = JsonConvert . DeserializeObject < JObject > ( content ) ;
1668+ if ( read == null )
15271669 {
1528- return true ;
1670+ // do not set the result to a null value, ever.
1671+
1672+ if ( isOptional )
1673+ {
1674+ return true ;
1675+ }
1676+
1677+ BeamableLogger . LogWarning ( $ "Config file was empty at { fullPath } !") ;
1678+ return false ;
15291679 }
1530- BeamableLogger . LogWarning ( $ "Config file was empty at { fullPath } !") ;
1531- return false ;
1532- }
15331680
1534- result = read ;
1535- return true ;
1681+ result = read ;
1682+ return true ;
1683+ }
1684+ catch ( Exception ex )
1685+ {
1686+ Log . Error ( ex . Message ) ;
1687+ throw ;
1688+ }
15361689 }
15371690
15381691 /// <summary>
0 commit comments