Repository navigation
Expand file tree
/
Copy pathFreeCADBootstrap.cs
More file actions
323 lines (296 loc) · 13.4 KB
/
Copy pathFreeCADBootstrap.cs
File metadata and controls
323 lines (296 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace AIOrchestrator.API;
/// <summary>
/// Locates a FreeCAD install and starts the bundled headless MCP bridge as a
/// background process, so the agent never has to start FreeCAD by hand. Mirrors
/// the AIOrchestrator <c>LocalExllamaV2Server</c> pattern: check-if-up,
/// start-if-not, wait-until-ready, thread-safe. On failure it returns a reason
/// key so the caller can show a localized OS notification.
/// </summary>
internal static class FreeCADBootstrap
{
private static readonly object Gate = new();
/// <summary>True when something is already listening on the bridge port.</summary>
public static bool IsBridgeUp(string host, int port)
{
try
{
using var c = new TcpClient();
var ar = c.BeginConnect(host, port, null, null);
if (!ar.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(1))) return false;
c.EndConnect(ar);
return true;
}
catch { return false; }
}
/// <summary>
/// Ensure the bridge is reachable, launching a headless FreeCAD if needed.
/// Returns null on success, or a reason key:
/// <c>"freecad_not_found"</c> | <c>"bridge_script_missing"</c> |
/// <c>"launch_failed"</c> | <c>"timeout"</c>.
/// </summary>
public static string? EnsureBridge(string host, int port)
{
if (IsBridgeUp(host, port)) return null;
lock (Gate)
{
if (IsBridgeUp(host, port)) return null;
var script = BridgeScriptPath();
if (script == null) return "bridge_script_missing";
var freecad = FindFreeCAD();
if (freecad == null) return "freecad_not_found";
try
{
var p = Process.Start(new ProcessStartInfo
{
FileName = freecad,
Arguments = $"\"{script}\"",
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = Path.GetDirectoryName(script)
});
p?.Dispose();
}
catch { return "launch_failed"; }
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(90);
while (DateTime.UtcNow < deadline)
{
if (IsBridgeUp(host, port)) return null;
Thread.Sleep(1000);
}
return "timeout";
}
}
// The bridge launcher ships next to the plugin DLL (Tools/FreeCADTool/).
private static string? BridgeScriptPath()
{
try
{
var dir = Path.GetDirectoryName(typeof(FreeCADTool).Assembly.Location);
if (string.IsNullOrEmpty(dir)) return null;
var p = Path.Combine(dir, "bridge_headless.py");
return File.Exists(p) ? p : null;
}
catch { return null; }
}
// The discovery walks directories, and a MISS would otherwise be repeated on every tool
// call, so the outcome is remembered for the process. FREECAD_CMD / FREECAD_HOME are read
// before the cache, so a host that sets them still wins without restarting anything.
private static readonly object DiscoveryGate = new();
private static string? _discovered;
private static bool _discoveredSet;
/// <summary>Locate the freecadcmd executable: the environment overrides first
/// (<c>FREECAD_CMD</c>, then <c>FREECAD_HOME</c>), then the search for this OS. Nothing
/// refuses to work because FreeCAD lives in an unusual place — the PATH, the standard
/// install folders and the fixed drives are all searched, and the two variables cover
/// everything else (an archive unpacked in a deep folder of its own, a Flatpak/Snap
/// sandbox). A negative result is logged with the hint, because that is the case a user
/// has to act on.</summary>
public static string? FindFreeCAD()
{
var cmd = Environment.GetEnvironmentVariable("FREECAD_CMD");
if (!string.IsNullOrWhiteSpace(cmd) && File.Exists(cmd)) return cmd;
var home = Environment.GetEnvironmentVariable("FREECAD_HOME");
if (!string.IsNullOrWhiteSpace(home))
{
var c = Path.Combine(home, CmdName());
if (File.Exists(c)) return c;
c = Path.Combine(home, "bin", CmdName());
if (File.Exists(c)) return c;
}
lock (DiscoveryGate)
{
if (_discoveredSet) return _discovered;
_discovered = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? FindWindows()
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? FindMac()
: FindLinux();
_discoveredSet = true;
Log.LogStep(_discovered != null
? $"FreeCADBootstrap: freecadcmd found at {_discovered}"
: "FreeCADBootstrap: freecadcmd not found (looked on PATH, in the usual install folders and on the fixed drives) — set FREECAD_CMD or FREECAD_HOME to the install");
return _discovered;
}
}
private static string CmdName() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "freecadcmd.exe" : "freecadcmd";
/// <summary>The command itself, found through PATH — the one place every OS agrees on, and
/// where a package manager (Linux) or a user who unpacked FreeCAD by hand puts it.</summary>
private static string? OnPath(params string[] names)
{
var path = Environment.GetEnvironmentVariable("PATH") ?? "";
foreach (var dir in path.Split(Path.PathSeparator))
{
if (string.IsNullOrWhiteSpace(dir)) continue;
foreach (var name in names)
{
try
{
var candidate = Path.Combine(dir.Trim().Trim('"'), name);
if (File.Exists(candidate)) return candidate;
}
catch { }
}
}
return null;
}
/// <summary>The user application-data directory of the FreeCAD the tool would use — asked OF
/// FreeCAD, not guessed. The layouts differ by install kind and version: the Windows installer
/// keeps it under <c>%APPDATA%\FreeCAD</c>, a distribution under <c>~/.local/share/FreeCAD</c>
/// or <c>~/Library/Application Support/FreeCAD</c>, and a PORTABLE archive — the official .7z,
/// unpacked on any drive — keeps it INSIDE the install folder (<c>…\data</c>). The chat's Mod
/// directory is <c><that>/Mod</c>, so a guessed path means the chat never loads and the
/// agent silently falls back to a headless FreeCAD. Runs the discovered <c>freecadcmd</c> once
/// on a small script and caches the answer; null when FreeCAD cannot be run, in which case the
/// caller falls back to the OS-standard locations.</summary>
public static string? GetUserAppDataDir()
{
lock (DiscoveryGate)
{
if (_userAppDataDirSet) return _userAppDataDir;
_userAppDataDirSet = true;
_userAppDataDir = QueryUserAppDataDir();
Log.LogStep(_userAppDataDir != null
? $"FreeCADBootstrap: FreeCAD user data dir is {_userAppDataDir}"
: "FreeCADBootstrap: FreeCAD could not report its user data dir (falling back to the OS-standard locations)");
return _userAppDataDir;
}
}
private static string? QueryUserAppDataDir()
{
var freecad = FindFreeCAD();
if (freecad == null) return null;
var script = Path.Combine(Path.GetTempPath(), "agentbridge_userdir.py");
try
{
File.WriteAllText(script, "import FreeCAD\nprint(FreeCAD.getUserAppDataDir())\n");
using var process = Process.Start(new ProcessStartInfo
{
FileName = freecad,
Arguments = "\"" + script + "\"",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
});
if (process == null) return null;
var stdout = process.StandardOutput.ReadToEnd();
process.WaitForExit(60_000);
foreach (var line in stdout.Split('\n'))
{
var candidate = line.Trim();
if (candidate.Length > 0 && Path.IsPathRooted(candidate) && Directory.Exists(candidate))
return candidate;
}
return null;
}
catch { return null; }
finally { try { File.Delete(script); } catch { } }
}
private static string? _userAppDataDir;
private static bool _userAppDataDirSet;
private static string? FindWindows()
{
const string name = "freecadcmd.exe";
var onPath = OnPath(name);
if (onPath != null) return onPath;
var roots = new List<string>();
foreach (var variable in new[] { "ProgramFiles", "ProgramFiles(x86)", "ProgramW6432" })
{
var value = Environment.GetEnvironmentVariable(variable);
if (!string.IsNullOrWhiteSpace(value)) roots.Add(value);
}
// A per-user install ("install for me only") lands here, not in Program Files.
var local = Environment.GetEnvironmentVariable("LOCALAPPDATA");
if (!string.IsNullOrWhiteSpace(local)) roots.Add(Path.Combine(local, "Programs"));
// The portable archives are unpacked anywhere, most often straight on a drive; every
// fixed drive is checked, so a FreeCAD on D: or E: is found exactly like one on C:.
try { foreach (var d in DriveInfo.GetDrives()) if (d.DriveType == DriveType.Fixed) roots.Add(d.RootDirectory.FullName); }
catch { }
foreach (var root in roots)
{
var hit = ScanRoot(root, name);
if (hit != null) return hit;
}
return null;
}
/// <summary>Searches one root for a FreeCAD install folder and returns the command inside it:
/// <c><root>/FreeCAD*/bin/<name></c> or <c><root>/FreeCAD*/<name></c>.
/// Both spellings the project uses are tried, because the installers and the official
/// archives disagree: <c>FreeCAD 1.0</c> on Windows and Fedora, <c>freecad-1.1</c> for the
/// unpacked tarball (Windows' search is case-insensitive, so one pass covers it there).</summary>
private static string? ScanRoot(string root, string name)
{
try
{
if (!Directory.Exists(root)) return null;
foreach (var pattern in new[] { "FreeCAD*", "freecad*" })
{
foreach (var dir in Directory.GetDirectories(root, pattern, SearchOption.TopDirectoryOnly))
{
var a = Path.Combine(dir, "bin", name);
if (File.Exists(a)) return a;
var b = Path.Combine(dir, name);
if (File.Exists(b)) return b;
}
}
}
catch { }
return null;
}
private static string? FindLinux()
{
// Fedora ships FreeCADCmd, Debian/Ubuntu freecadcmd, and some packages symlink
// freecad-cmd: all three are the same program under a different name.
var onPath = OnPath("freecadcmd", "FreeCADCmd", "freecad-cmd");
if (onPath != null) return onPath;
foreach (var c in new[]
{
"/usr/bin/freecadcmd", "/usr/bin/freecad-cmd", "/usr/bin/FreeCADCmd",
"/usr/local/bin/freecadcmd", "/usr/local/bin/FreeCADCmd",
"/snap/bin/freecadcmd",
})
if (File.Exists(c)) return c;
// The layouts that keep the program outside the PATH: /opt/freecad-1.1 (the unpacked
// official archive, including the AppImage extract) and /usr/lib/freecad* (the
// Ubuntu PPA). A Flatpak or Snap sandbox cannot be driven from outside — the bridge
// has to run inside that sandbox, and the plugin cannot start it — which is what
// FREECAD_CMD / FREECAD_HOME are for when the user installed it another way.
foreach (var root in new[] { "/opt", "/usr/lib" })
{
var hit = ScanRoot(root, "freecadcmd") ?? ScanRoot(root, "FreeCADCmd");
if (hit != null) return hit;
}
return null;
}
private static string? FindMac()
{
var onPath = OnPath("freecadcmd");
if (onPath != null) return onPath;
// /Applications needs an admin install; ~/Applications is the per-user one.
var applicationDirs = new List<string>();
var home = Environment.GetEnvironmentVariable("HOME");
if (!string.IsNullOrWhiteSpace(home)) applicationDirs.Add(Path.Combine(home, "Applications"));
applicationDirs.Add("/Applications");
foreach (var dir in applicationDirs)
{
foreach (var app in new[] { "FreeCAD.app", "FreeCAD 1.app", "FreeCAD 0.20.app", "FreeCAD 0.21.app" })
{
var c = Path.Combine(dir, app, "Contents/Resources/bin/freecadcmd");
if (File.Exists(c)) return c;
}
try
{
foreach (var bundle in Directory.GetDirectories(dir, "FreeCAD*.app", SearchOption.TopDirectoryOnly))
{
var c = Path.Combine(bundle, "Contents/Resources/bin/freecadcmd");
if (File.Exists(c)) return c;
}
}
catch { }
}
return null;
}
}