Skip to content

Commit 4e2d513

Browse files
authored
DYN-9707 : Show warning when a file is opened in another Dynamo instance (#17139)
1 parent 2900406 commit 4e2d513

17 files changed

Lines changed: 1390 additions & 57 deletions
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.IO;
4+
using System.Security;
5+
using System.Text;
6+
using System.Threading;
7+
using Newtonsoft.Json;
8+
9+
namespace Dynamo.Graph.Workspaces.Locking
10+
{
11+
/// <summary>
12+
/// Provides file-system operations for graph lock sidecar files.
13+
/// </summary>
14+
internal static class GraphLockFile
15+
{
16+
private static readonly JsonSerializer Serializer = JsonSerializer.Create(new JsonSerializerSettings
17+
{
18+
Culture = System.Globalization.CultureInfo.InvariantCulture,
19+
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
20+
Formatting = Formatting.Indented,
21+
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
22+
MissingMemberHandling = MissingMemberHandling.Ignore,
23+
NullValueHandling = NullValueHandling.Ignore,
24+
TypeNameHandling = TypeNameHandling.None
25+
});
26+
27+
/// <summary>
28+
/// Gets the sidecar lock path for a graph file.
29+
/// </summary>
30+
/// <param name="graphPath">The graph file path.</param>
31+
/// <returns>The sidecar lock file path.</returns>
32+
internal static string GetLockFilePath(string graphPath)
33+
{
34+
return Path.GetFullPath(graphPath) + ".dynlock";
35+
}
36+
37+
/// <summary>
38+
/// Attempts to create a lock file only if it does not already exist.
39+
/// </summary>
40+
/// <param name="sidecarPath">The sidecar lock file path.</param>
41+
/// <param name="info">The lock metadata to write.</param>
42+
/// <returns>True if the lock file was created, false if one already exists.</returns>
43+
internal static bool TryCreateNewLockFile(string sidecarPath, GraphLockInfo info)
44+
{
45+
try
46+
{
47+
using (var stream = new FileStream(sidecarPath, FileMode.CreateNew, FileAccess.Write, FileShare.Read))
48+
using (var writer = new StreamWriter(stream, new UTF8Encoding(false)))
49+
{
50+
Serializer.Serialize(writer, info);
51+
}
52+
53+
return true;
54+
}
55+
catch (IOException) when (File.Exists(sidecarPath))
56+
{
57+
return false;
58+
}
59+
}
60+
61+
62+
/// <summary>
63+
/// Attempts to read graph-lock metadata from a sidecar file.
64+
/// </summary>
65+
/// <param name="sidecarPath">The sidecar lock file path.</param>
66+
/// <param name="info">The parsed lock metadata when reading succeeds.</param>
67+
/// <returns>True if metadata was read; otherwise false.</returns>
68+
internal static bool TryRead(string sidecarPath, out GraphLockInfo info)
69+
{
70+
info = null;
71+
72+
const int maxAttempts = 2;
73+
for (var attempt = 0; attempt < maxAttempts; attempt++)
74+
{
75+
try
76+
{
77+
using (var stream = File.Open(sidecarPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
78+
using (var reader = new StreamReader(stream, Encoding.UTF8))
79+
using (var jsonReader = new JsonTextReader(reader))
80+
{
81+
info = Serializer.Deserialize<GraphLockInfo>(jsonReader);
82+
}
83+
84+
return info != null;
85+
}
86+
catch (Exception ex) when (ex is IOException || ex is JsonException)
87+
{
88+
if (attempt == maxAttempts - 1)
89+
{
90+
return false;
91+
}
92+
93+
// Wait for 50 milliseconds before retrying
94+
Thread.Sleep(50);
95+
}
96+
catch (UnauthorizedAccessException)
97+
{
98+
return false;
99+
}
100+
catch (SecurityException)
101+
{
102+
return false;
103+
}
104+
}
105+
106+
return false;
107+
}
108+
109+
/// <summary>
110+
/// Updates a lock file heartbeat using a temp file and replace operation.
111+
/// </summary>
112+
/// <param name="sidecarPath">The sidecar lock file path.</param>
113+
/// <param name="info">The lock metadata to write.</param>
114+
internal static void WriteHeartbeat(string sidecarPath, GraphLockInfo info)
115+
{
116+
var tempPath = sidecarPath + ".tmp";
117+
var replaced = false;
118+
119+
try
120+
{
121+
using (var stream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None))
122+
using (var writer = new StreamWriter(stream, new UTF8Encoding(false)))
123+
{
124+
Serializer.Serialize(writer, info);
125+
}
126+
127+
try
128+
{
129+
File.Replace(tempPath, sidecarPath, null, true);
130+
}
131+
catch (Exception ex) when (ex is IOException || ex is PlatformNotSupportedException || ex is UnauthorizedAccessException)
132+
{
133+
File.Move(tempPath, sidecarPath, true);
134+
}
135+
136+
replaced = true;
137+
}
138+
finally
139+
{
140+
// If the swap never completed (e.g. serialization threw), don't leave a stray scratch file.
141+
if (!replaced && File.Exists(tempPath))
142+
{
143+
TryDelete(tempPath);
144+
}
145+
}
146+
}
147+
148+
/// <summary>
149+
/// Attempts to delete a sidecar lock file without throwing.
150+
/// </summary>
151+
/// <param name="sidecarPath">The sidecar lock file path.</param>
152+
internal static void TryDelete(string sidecarPath)
153+
{
154+
try
155+
{
156+
File.Delete(sidecarPath);
157+
}
158+
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is SecurityException)
159+
{
160+
Debug.WriteLine("GraphLock cleanup delete failed: " + ex.Message);
161+
}
162+
}
163+
}
164+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Newtonsoft.Json;
2+
using System;
3+
4+
namespace Dynamo.Graph.Workspaces.Locking
5+
{
6+
/// <summary>
7+
/// Serializable metadata stored in a graph lock sidecar file.
8+
/// </summary>
9+
internal sealed class GraphLockInfo
10+
{
11+
[JsonProperty("sessionId")]
12+
internal Guid SessionId { get; set; }
13+
14+
[JsonProperty("graphPath")]
15+
internal string GraphPath { get; set; }
16+
17+
[JsonProperty("machineName")]
18+
internal string MachineName { get; set; }
19+
20+
[JsonProperty("processId")]
21+
internal int ProcessId { get; set; }
22+
23+
[JsonProperty("processStartUtc")]
24+
internal DateTime ProcessStartUtc { get; set; }
25+
26+
[JsonProperty("lastHeartbeatUtc")]
27+
internal DateTime LastHeartbeatUtc { get; set; }
28+
}
29+
}

0 commit comments

Comments
 (0)