33f5ad68bf
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m19s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m26s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m32s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m34s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m57s
Project CI / AI game creator shell Rust crates (push) Successful in 3m18s
Project CI / Native shell tests (push) Successful in 10m54s
Project CI / Backend tests (push) Successful in 12m42s
Project CI / Frontend tests (push) Successful in 13m4s
Project CI / AI game creator shell web tests (push) Successful in 5m1s
Project CI / Repository checks (push) Successful in 12m48s
AGC 原有插件系统无法直接操作已打开的 Unity Editor。本变更增加内置 `agc-unity-editor`,在 Windows x64 / Unity Mono 上支持当前项目探测、连接与 C# 执行,不向 Unity 工程安装 UPM 桥接包。 ## 主要变更 - 固定复用 DotCraft.Unity 0.4.3 的 Attach 核心,提供自包含 .NET helper,保留上游许可证、来源及修改记录。 - GUI、Runtime、DirectProject 共用 Runner 执行服务;补齐项目身份、并发、总期限、回执确认与持久不确定状态阻断。 - 现有打开项目入口支持 Unity,按项目类型及开关暴露插件和 Agent 工具。 - Windows 构建准备 helper 并随包分发;插件 JS/Rust 测试接入现有 CI 组,Jenkins 增加 .NET 10 工具链预检。 ## 验证 - .NET helper 27 项测试、自包含发布及最小环境协议 smoke 通过。 - Unity 6000.3.7f1 实机验证通过:连接、C# 执行、编译错误修复、断连重连、Domain Reload 后重新握手;真实 Runner 的 ACK、并发拒绝和跨重启阻断通过。 - 宿主 Unity、PluginHost、Cocos、MCP、工具目录与引擎识别定向回归通过;前端类型检查、插件 JS/Rust、CI 配置、格式、编码和文档门禁通过。 Linux CI 不代替 Windows helper/实机验证;发行安装包 UI smoke、其它 Unity 版本和 Unity CoreCLR 未验证。Unity 演示工程中的场景和组件已撤销,不在此 PR 范围内。 --------- Co-authored-by: kdletters <61648117+kdletters@users.noreply.github.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/423
339 lines
20 KiB
C#
339 lines
20 KiB
C#
using System.Text;
|
|
using System.Text.Json.Nodes;
|
|
using Agc.Unity.Attach;
|
|
|
|
var tests = new (string Name, Func<Task> Run)[]
|
|
{
|
|
("detect is read-only and disconnected", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
var result = Result(await f.Call("detect"));
|
|
Equal(false, result["connected"]!.GetValue<bool>());
|
|
Equal(123, result["pid"]!.GetValue<int>());
|
|
Equal(0, f.Backend.ConnectCalls);
|
|
}),
|
|
("connection publishes verified identity", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
var result = Result(await f.Call("connect"));
|
|
Equal(true, result["connected"]!.GetValue<bool>());
|
|
Equal("unity-editor", result["adapter"]!.GetValue<string>());
|
|
Equal("generation-1", result["generation"]!.GetValue<string>());
|
|
Check(result["startedUtc"] != null);
|
|
}),
|
|
("cross-project PID cannot attach", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Targets = [f.Backend.Targets[0] with { ProjectPath = Path.GetTempPath() }];
|
|
var request = f.Request("execute"); request["params"]!["processId"] = 123;
|
|
var result = Result(await f.Host.HandleAsync(request));
|
|
Equal("failed", result["status"]!.GetValue<string>());
|
|
Equal(false, result["dispatched"]!.GetValue<bool>());
|
|
Equal(0, f.Backend.ConnectCalls);
|
|
}),
|
|
("ambiguous target is rejected without injection", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Targets = [f.Backend.Targets[0], f.Backend.Targets[0] with { Pid = 124 }];
|
|
Equal("UnityTargetAmbiguous", (await f.Call("connect"))["error"]!["code"]!.GetValue<string>());
|
|
Equal(0, f.Backend.ConnectCalls);
|
|
}),
|
|
("PID start identity cannot change during connect", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.OnConnect = () => f.Backend.Targets = [f.Backend.Targets[0] with { StartedUtc = DateTime.UtcNow }];
|
|
Equal("UnityTargetChanged", (await f.Call("connect"))["error"]!["code"]!.GetValue<string>());
|
|
Equal(false, Result(await f.Call("status"))["connected"]!.GetValue<bool>());
|
|
}),
|
|
("handshake project mismatch rejects connection", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.MetadataProject = Path.GetTempPath();
|
|
Equal("UnityHandshakeInvalid", (await f.Call("connect"))["error"]!["code"]!.GetValue<string>());
|
|
}),
|
|
("failed connect clears previous connection", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
await f.Call("connect");
|
|
var request = f.Request("connect"); request["params"]!["projectPath"] = "relative";
|
|
Check((await f.Host.HandleAsync(request))["error"] != null);
|
|
Equal(false, Result(await f.Call("status"))["connected"]!.GetValue<bool>());
|
|
}),
|
|
("actual result including null is preserved", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Execute = (dispatch, _) => { dispatch(); return Task.FromResult(FakeBackend.Completed(null)); };
|
|
var result = Result(await f.Call("execute"));
|
|
Equal("completed", result["status"]!.GetValue<string>());
|
|
Equal(true, result["dispatched"]!.GetValue<bool>());
|
|
Check(result.ContainsKey("result") && result["result"] == null);
|
|
}),
|
|
("compiler failure is known unsent and allows repaired code", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Execute = (_, _) => throw new InvalidOperationException("CS0103: missing identifier");
|
|
var result = Result(await f.Call("execute"));
|
|
Equal("failed", result["status"]!.GetValue<string>()); Equal(false, result["dispatched"]!.GetValue<bool>());
|
|
f.Backend.Execute = FakeBackend.Succeed;
|
|
Equal("completed", Result(await f.Call("execute"))["status"]!.GetValue<string>());
|
|
}),
|
|
("known runtime failure does not latch", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Execute = (dispatch, _) => { dispatch(); return Task.FromResult(new JsonObject { ["state"] = "failed", ["generation"] = "g", ["executionId"] = "e", ["error"] = "A runtime exception" }); };
|
|
var result = Result(await f.Call("execute"));
|
|
Equal("failed", result["status"]!.GetValue<string>()); Equal(true, result["dispatched"]!.GetValue<bool>());
|
|
f.Backend.Execute = FakeBackend.Succeed;
|
|
Equal("completed", Result(await f.Call("execute"))["status"]!.GetValue<string>());
|
|
}),
|
|
("unknown is never replayed and disconnect cannot unlatch", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Execute = (dispatch, _) => { dispatch(); return Task.FromResult(new JsonObject { ["state"] = "unknown" }); };
|
|
Equal("needs-reconciliation", Result(await f.Call("execute"))["status"]!.GetValue<string>());
|
|
await f.Call("disconnect");
|
|
Equal("needs-reconciliation", Result(await f.Call("execute"))["status"]!.GetValue<string>());
|
|
Equal(1, f.Backend.ExecuteCalls);
|
|
}),
|
|
("lost execution receipt latches", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Execute = (dispatch, _) => { dispatch(); return Task.FromResult(new JsonObject { ["state"] = "lost" }); };
|
|
Equal("needs-reconciliation", Result(await f.Call("execute"))["status"]!.GetValue<string>());
|
|
}),
|
|
("malformed receipt latches", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Execute = (dispatch, _) => { dispatch(); return Task.FromResult(new JsonObject { ["state"] = "completed" }); };
|
|
Equal("needs-reconciliation", Result(await f.Call("execute"))["status"]!.GetValue<string>());
|
|
}),
|
|
("wait completion does not resend execution", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Execute = (dispatch, _) => { dispatch(); return Task.FromResult(new JsonObject { ["state"] = "running", ["generation"] = "generation-1", ["executionId"] = "execution-1" }); };
|
|
Equal("completed", Result(await f.Call("execute"))["status"]!.GetValue<string>());
|
|
Equal(1, f.Backend.ExecuteCalls); Equal(1, f.Backend.WaitCalls);
|
|
}),
|
|
("post-dispatch timeout latches within deadline", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Execute = async (dispatch, token) => { dispatch(); await Task.Delay(Timeout.Infinite, token); return FakeBackend.Completed(1); };
|
|
var request = f.Request("execute"); request["params"]!["timeoutMs"] = 40;
|
|
var timer = System.Diagnostics.Stopwatch.StartNew();
|
|
Equal("needs-reconciliation", Result(await f.Host.HandleAsync(request))["status"]!.GetValue<string>());
|
|
Check(timer.Elapsed < TimeSpan.FromSeconds(2));
|
|
Equal("needs-reconciliation", Result(await f.Call("execute"))["status"]!.GetValue<string>());
|
|
}),
|
|
("late compiler cannot dispatch after timeout", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var finished = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var sent = false;
|
|
f.Backend.Execute = async (dispatch, _) =>
|
|
{
|
|
entered.SetResult(); await release.Task;
|
|
try { dispatch(); sent = true; return FakeBackend.Completed(1); }
|
|
finally { finished.SetResult(); }
|
|
};
|
|
var request = f.Request("execute"); request["params"]!["timeoutMs"] = 100;
|
|
var pending = f.Host.HandleAsync(request);
|
|
await entered.Task;
|
|
var result = Result(await pending);
|
|
Equal("failed", result["status"]!.GetValue<string>()); Equal(false, result["dispatched"]!.GetValue<bool>());
|
|
Equal("UnityBusy", Result(await f.Call("execute"))["error"]!["code"]!.GetValue<string>());
|
|
release.SetResult(); await finished.Task;
|
|
Equal(false, sent);
|
|
}),
|
|
("concurrent execution is rejected, not queued", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
f.Backend.Execute = async (dispatch, _) => { dispatch(); entered.SetResult(); await release.Task; return FakeBackend.Completed(1); };
|
|
var first = f.Call("execute"); await entered.Task;
|
|
var second = Result(await f.Call("execute"));
|
|
Equal("UnityBusy", second["error"]!["code"]!.GetValue<string>());
|
|
Equal(false, second["dispatched"]!.GetValue<bool>());
|
|
release.SetResult(); await first; Equal(1, f.Backend.ExecuteCalls);
|
|
}),
|
|
("UTF-8 code limit, NUL and payload path rejected", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
foreach (var code in new[] { "", "a\0b", new string('中', 44_000) })
|
|
{
|
|
var request = f.Request("execute"); request["params"]!["code"] = code;
|
|
Equal(false, Result(await f.Host.HandleAsync(request))["dispatched"]!.GetValue<bool>());
|
|
}
|
|
var arbitrary = f.Request("execute"); arbitrary["params"]!["payloadPath"] = "bad.dll";
|
|
Equal(false, Result(await f.Host.HandleAsync(arbitrary))["dispatched"]!.GetValue<bool>());
|
|
Equal(0, f.Backend.ExecuteCalls);
|
|
}),
|
|
("timeout and protocol bounds are validated", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
foreach (var timeout in new[] { 0, -1, 60_001 })
|
|
{
|
|
var request = f.Request("execute"); request["params"]!["timeoutMs"] = timeout;
|
|
Equal(false, Result(await f.Host.HandleAsync(request))["dispatched"]!.GetValue<bool>());
|
|
}
|
|
var badId = f.Request("detect"); badId["id"] = 0;
|
|
Equal("UnityIdInvalid", (await f.Host.HandleAsync(badId))["error"]!["code"]!.GetValue<string>());
|
|
var badProtocol = f.Request("detect"); badProtocol["protocol"] = "other";
|
|
Check((await f.Host.HandleAsync(badProtocol))["error"] != null);
|
|
}),
|
|
("oversized completed result latches before accepting another execution", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Execute = (dispatch, _) => { dispatch(); return Task.FromResult(FakeBackend.Completed(new string('x', HelperHost.MaximumMessageBytes))); };
|
|
Equal("needs-reconciliation", Result(await f.Call("execute"))["status"]!.GetValue<string>());
|
|
Equal("needs-reconciliation", Result(await f.Call("execute"))["status"]!.GetValue<string>());
|
|
Equal(1, f.Backend.ExecuteCalls);
|
|
}),
|
|
("diagnostics redact credentials", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
f.Backend.Execute = (_, _) => throw new InvalidOperationException("Bearer secret123 api_key=xyz token: abc");
|
|
var result = Result(await f.Call("execute")).ToJsonString();
|
|
Check(!result.Contains("secret123") && !result.Contains("xyz") && !result.Contains("abc"));
|
|
}),
|
|
("compiler diagnostics redact slash and backslash cache paths without losing locations", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
var local = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
|
var cache = Path.Combine(local, "Genarrative", "UnityAttach");
|
|
foreach (var separator in new[] { '/', '\\' })
|
|
{
|
|
var source = Path.Combine(cache, "snippets", "example.cs").Replace('\\', separator).Replace('/', separator);
|
|
f.Backend.Execute = (_, _) => throw new InvalidOperationException(source + "(1,8): error CS0103: The name 'MissingValue' does not exist in the current context");
|
|
var message = Result(await f.Call("execute"))["error"]!["message"]!.GetValue<string>();
|
|
Check(message.StartsWith("<unity-cache>"));
|
|
Check(!message.Contains(local, StringComparison.OrdinalIgnoreCase));
|
|
Check(message.Contains("example.cs(1,8): error CS0103: The name 'MissingValue' does not exist in the current context"));
|
|
}
|
|
}),
|
|
("redirected per-user compiler path is redacted while error prose remains", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
var profile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile).Replace('\\', '/');
|
|
var source = profile + "/AppData/Local/Packages/Example.Host/LocalCache/Local/Genarrative/UnityAttach/snippets/example.cs";
|
|
f.Backend.Execute = (_, _) => throw new InvalidOperationException(source + "(3,14): error CS1002: ; expected");
|
|
var message = Result(await f.Call("execute"))["error"]!["message"]!.GetValue<string>();
|
|
Check(!message.Contains(profile, StringComparison.OrdinalIgnoreCase));
|
|
Check(message.Contains("example.cs(3,14): error CS1002: ; expected"));
|
|
Check(message.StartsWith("<user>") || message.StartsWith("<local>"));
|
|
}),
|
|
("NDJSON validates malformed UTF-8 and honors message bound", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
using var input = new MemoryStream([0xff, (byte)'\n']); using var output = new MemoryStream();
|
|
await ProtocolServer.RunAsync(input, output, f.Host);
|
|
Check(Encoding.UTF8.GetString(output.ToArray()).Contains("UnityProtocolInvalid"));
|
|
using var oversized = new MemoryStream(new byte[HelperHost.MaximumMessageBytes + 1]);
|
|
var reader = new BoundedLineReader(oversized, HelperHost.MaximumMessageBytes);
|
|
try { await reader.ReadAsync(); throw new Exception("Expected message limit"); }
|
|
catch (HostException error) { Equal("UnityMessageTooLarge", error.Code); }
|
|
}),
|
|
("NDJSON round trip echoes id and protocol", async () =>
|
|
{
|
|
await using var f = new Fixture();
|
|
using var input = new MemoryStream(Encoding.UTF8.GetBytes(f.Request("detect").ToJsonString() + "\n"));
|
|
using var output = new MemoryStream();
|
|
await ProtocolServer.RunAsync(input, output, f.Host);
|
|
var reply = JsonNode.Parse(output.ToArray())!;
|
|
Equal(HelperHost.Protocol, reply["protocol"]!.GetValue<string>());
|
|
Equal(1L, reply["id"]!.GetValue<long>());
|
|
}),
|
|
("Windows canonical extended prefix compares equally", () =>
|
|
{
|
|
if (OperatingSystem.IsWindows()) Check(ProjectIdentity.Same(@"\\?\C:\Unity\Project", @"C:\Unity\Project\"));
|
|
return Task.CompletedTask;
|
|
}),
|
|
("Windows short and long existing paths share physical identity", async () =>
|
|
{
|
|
if (!OperatingSystem.IsWindows()) return;
|
|
await using var f = new Fixture();
|
|
var longPath = ProjectIdentity.Normalize(f.Project);
|
|
var buffer = new StringBuilder(32768);
|
|
var length = NativePaths.GetShortPathNameW(longPath, buffer, (uint)buffer.Capacity);
|
|
Check(length > 0 && length < buffer.Capacity);
|
|
var shortPath = buffer.ToString();
|
|
Check(ProjectIdentity.Same(shortPath, longPath));
|
|
Equal(longPath, ProjectIdentity.Normalize(shortPath));
|
|
f.Backend.Targets = [f.Backend.Targets[0] with { ProjectPath = shortPath }];
|
|
var request = f.Request("connect"); request["params"]!["projectPath"] = longPath;
|
|
Equal(true, Result(await f.Host.HandleAsync(request))["connected"]!.GetValue<bool>());
|
|
})
|
|
};
|
|
|
|
var failures = 0;
|
|
foreach (var test in tests)
|
|
{
|
|
try { await test.Run().WaitAsync(TimeSpan.FromSeconds(10)); Console.WriteLine("PASS " + test.Name); }
|
|
catch (Exception error) { failures++; Console.Error.WriteLine("FAIL " + test.Name + ": " + error); }
|
|
}
|
|
Console.WriteLine($"Unity helper: {tests.Length - failures}/{tests.Length} passed (fake Editor transport; no Unity injection).");
|
|
return failures == 0 ? 0 : 1;
|
|
|
|
static JsonObject Result(JsonObject response) => response["result"]?.AsObject() ?? throw new Exception(response.ToJsonString());
|
|
static void Check(bool condition) { if (!condition) throw new Exception("Assertion failed"); }
|
|
static void Equal<T>(T expected, T actual) { if (!EqualityComparer<T>.Default.Equals(expected, actual)) throw new Exception($"Expected {expected}; got {actual}"); }
|
|
|
|
sealed class Fixture : IAsyncDisposable
|
|
{
|
|
public string Project { get; } = Path.Combine(Path.GetTempPath(), "agc-unity-helper-tests-" + Guid.NewGuid().ToString("N"));
|
|
public FakeBackend Backend { get; }
|
|
public HelperHost Host { get; }
|
|
private long nextId;
|
|
public Fixture()
|
|
{
|
|
foreach (var name in new[] { "Assets", "Packages", "ProjectSettings" }) Directory.CreateDirectory(Path.Combine(Project, name));
|
|
File.WriteAllText(Path.Combine(Project, "ProjectSettings", "ProjectVersion.txt"), "m_EditorVersion: 2022.3.0f1\n");
|
|
Backend = new FakeBackend(Project); Host = new HelperHost(Backend);
|
|
}
|
|
public JsonObject Request(string method) => new()
|
|
{
|
|
["jsonrpc"] = "2.0", ["protocol"] = HelperHost.Protocol, ["id"] = Interlocked.Increment(ref nextId), ["method"] = method,
|
|
["params"] = method == "execute" ? new JsonObject { ["projectPath"] = Project, ["code"] = "return 42;" } : new JsonObject { ["projectPath"] = Project }
|
|
};
|
|
public Task<JsonObject> Call(string method) => Host.HandleAsync(Request(method));
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await Host.DisposeAsync();
|
|
if (!Path.GetFullPath(Project).StartsWith(Path.GetFullPath(Path.GetTempPath()), StringComparison.OrdinalIgnoreCase)
|
|
|| !Path.GetFileName(Project).StartsWith("agc-unity-helper-tests-", StringComparison.Ordinal)) throw new Exception("Unsafe test cleanup path");
|
|
Directory.Delete(Project, true);
|
|
}
|
|
}
|
|
|
|
sealed class FakeBackend : IEditorBackend
|
|
{
|
|
public IReadOnlyList<EditorTarget> Targets;
|
|
public string MetadataProject;
|
|
public int ConnectCalls, ExecuteCalls, WaitCalls;
|
|
public Action? OnConnect;
|
|
public Func<Action, CancellationToken, Task<JsonObject>> Execute = Succeed;
|
|
public FakeBackend(string project)
|
|
{
|
|
MetadataProject = project;
|
|
Targets = [new EditorTarget(123, new DateTime(2026, 9, 18, 0, 0, 0, DateTimeKind.Utc), project, "Unity.exe")];
|
|
}
|
|
public static JsonObject Completed(JsonNode? result) => new() { ["state"] = "completed", ["generation"] = "generation-1", ["executionId"] = "execution-1", ["result"] = result };
|
|
public static Task<JsonObject> Succeed(Action dispatch, CancellationToken token) { token.ThrowIfCancellationRequested(); dispatch(); return Task.FromResult(Completed(42)); }
|
|
public Task<IReadOnlyList<EditorTarget>> DetectAsync(CancellationToken token) { token.ThrowIfCancellationRequested(); return Task.FromResult(Targets); }
|
|
public Task<JsonObject> ConnectAsync(EditorTarget target, CancellationToken token) { ConnectCalls++; OnConnect?.Invoke(); return StatusAsync(token); }
|
|
public Task<JsonObject> StatusAsync(CancellationToken token) => Task.FromResult(new JsonObject { ["state"] = "completed", ["generation"] = "generation-1", ["result"] = new JsonObject { ["project"] = MetadataProject, ["version"] = "2022.3.0f1" } });
|
|
public Task<JsonObject> ExecuteAsync(string code, Action onDispatch, CancellationToken token) { ExecuteCalls++; return Execute(onDispatch, token); }
|
|
public Task<JsonObject> WaitAsync(string id, CancellationToken token) { WaitCalls++; return Task.FromResult(Completed(42)); }
|
|
public Task DisconnectAsync(CancellationToken token) => Task.CompletedTask;
|
|
public void ForgetSelection() { }
|
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
}
|
|
|
|
static class NativePaths
|
|
{
|
|
[System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode, SetLastError = true)]
|
|
internal static extern uint GetShortPathNameW(string longPath, StringBuilder shortPath, uint size);
|
|
}
|