diff options
Diffstat (limited to 'libs/VNLib.Plugins.Sessions.Memory/src')
7 files changed, 0 insertions, 651 deletions
diff --git a/libs/VNLib.Plugins.Sessions.Memory/src/MemSessionPluginEntry.cs b/libs/VNLib.Plugins.Sessions.Memory/src/MemSessionPluginEntry.cs deleted file mode 100644 index 0374243..0000000 --- a/libs/VNLib.Plugins.Sessions.Memory/src/MemSessionPluginEntry.cs +++ /dev/null @@ -1,71 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Sessions.Memory -* File: MemSessionPluginEntry.cs -* -* MemSessionPluginEntry.cs is part of VNLib.Plugins.Sessions.Memory which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Sessions.Memory is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Sessions.Memory is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Threading; -using System.Threading.Tasks; - -using VNLib.Net.Http; -using VNLib.Utils.Logging; -using VNLib.Plugins.Essentials.Sessions; - -namespace VNLib.Plugins.Sessions.Memory -{ - /// <summary> - /// Provides an IPlugin entrypoint for standalone memory sessions - /// </summary> - public sealed class MemSessionPluginEntry : PluginBase, ISessionProvider - { - ///<inheritdoc/> - public override string PluginName => "Essentials.MemorySessions"; - - private readonly MemorySessionEntrypoint ep = new(); - - ///<inheritdoc/> - protected override void OnLoad() - { - //Try to load - ep.Load(this, Log); - Log.Information("Plugin loaded"); - } - - ///<inheritdoc/> - protected override void OnUnLoad() - { - Log.Information("Plugin unloaded"); - } - - ///<inheritdoc/> - protected override void ProcessHostCommand(string cmd) - { - throw new NotImplementedException(); - } - - ///<inheritdoc/> - public ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) - { - return ep.GetSessionAsync(entity, cancellationToken); - } - } -} diff --git a/libs/VNLib.Plugins.Sessions.Memory/src/MemorySession.cs b/libs/VNLib.Plugins.Sessions.Memory/src/MemorySession.cs deleted file mode 100644 index 5033362..0000000 --- a/libs/VNLib.Plugins.Sessions.Memory/src/MemorySession.cs +++ /dev/null @@ -1,129 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Sessions.Memory -* File: MemorySession.cs -* -* MemorySession.cs is part of VNLib.Plugins.Essentials.Sessions.Memory which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Sessions.Memory is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Sessions.Memory is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Net; -using System.Threading.Tasks; -using System.Collections.Generic; -using VNLib.Plugins.Essentials.Extensions; - -using VNLib.Utils.Async; -using VNLib.Net.Http; -using VNLib.Utils.Memory.Caching; -using VNLib.Plugins.Essentials.Sessions; -using static VNLib.Plugins.Essentials.Sessions.ISessionExtensions; - -namespace VNLib.Plugins.Sessions.Memory -{ - internal class MemorySession : SessionBase, ICacheable - { - private readonly Dictionary<string, string> DataStorage; - - private readonly Func<IHttpEvent, string, string> OnSessionUpdate; - private readonly AsyncQueue<MemorySession> ExpiredTable; - - public MemorySession(string sessionId, IPAddress ipAddress, Func<IHttpEvent, string, string> onSessionUpdate, AsyncQueue<MemorySession> expired) - { - //Set the initial is-new flag - DataStorage = new Dictionary<string, string>(10); - ExpiredTable = expired; - - OnSessionUpdate = onSessionUpdate; - //Get new session id - SessionID = sessionId; - UserIP = ipAddress; - SessionType = SessionType.Web; - Created = DateTimeOffset.UtcNow; - //Init - IsNew = true; - } - //Store in memory directly - public override IPAddress UserIP { get; protected set; } - - //Session type has no backing store, so safe to hard-code it's always web - - public override SessionType SessionType => SessionType.Web; - - protected override ValueTask<Task?> UpdateResource(bool isAsync, IHttpEvent state) - { - //if invalid is set, invalide the current session - if (Flags.IsSet(INVALID_MSK)) - { - //Clear storage, and regenerate the sessionid - DataStorage.Clear(); - //store new sessionid - SessionID = OnSessionUpdate(state, SessionID); - //Reset ip-address - UserIP = state.Server.GetTrustedIp(); - //Update created-time - Created = DateTimeOffset.UtcNow; - //Re-initialize the session to the state of the current connection - this.InitNewSession(state.Server); - //Modified flag doesnt matter since there is no write-back - - } - else if (Flags.IsSet(REGEN_ID_MSK)) - { - //Regen id without modifying the data store - SessionID = OnSessionUpdate(state, SessionID); - } - //Clear flags - Flags.ClearAll(); - //Memory session always completes - return ValueTask.FromResult<Task?>(null); - } - - protected override string IndexerGet(string key) - { - return DataStorage.GetValueOrDefault(key, string.Empty); - } - - protected override void IndexerSet(string key, string value) - { - //Check for special keys - switch (key) - { - //For token/login hashes, we can set the upgrade flag - case LOGIN_TOKEN_ENTRY: - case TOKEN_ENTRY: - Flags.Set(REGEN_ID_MSK); - break; - } - DataStorage[key] = value; - } - - - DateTime ICacheable.Expires { get; set; } - - void ICacheable.Evicted() - { - DataStorage.Clear(); - //Enque cleanup - _ = ExpiredTable.TryEnque(this); - } - - bool IEquatable<ICacheable>.Equals(ICacheable? other) => other is ISession ses && SessionID.Equals(ses.SessionID, StringComparison.Ordinal); - - } -} diff --git a/libs/VNLib.Plugins.Sessions.Memory/src/MemorySessionConfig.cs b/libs/VNLib.Plugins.Sessions.Memory/src/MemorySessionConfig.cs deleted file mode 100644 index b330bb1..0000000 --- a/libs/VNLib.Plugins.Sessions.Memory/src/MemorySessionConfig.cs +++ /dev/null @@ -1,58 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Sessions.Memory -* File: MemorySessionConfig.cs -* -* MemorySessionConfig.cs is part of VNLib.Plugins.Essentials.Sessions.Memory which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Sessions.Memory is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Sessions.Memory is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; - -using VNLib.Utils.Logging; - -namespace VNLib.Plugins.Sessions.Memory -{ - /// <summary> - /// Represents configration variables used to create and operate http sessions. - /// </summary> - public readonly struct MemorySessionConfig - { - /// <summary> - /// The name of the cookie to use for matching sessions - /// </summary> - public string SessionCookieID { get; init; } - /// <summary> - /// The size (in bytes) of the genreated SessionIds - /// </summary> - public uint SessionIdSizeBytes { get; init; } - /// <summary> - /// The amount of time a session is valid (within the backing store) - /// </summary> - public TimeSpan SessionTimeout { get; init; } - /// <summary> - /// The log for which all errors within the <see cref="SessionProvider"/> instance will be written to. - /// </summary> - public ILogProvider SessionLog { get; init; } - /// <summary> - /// The maximum number of sessions allowed to be cached in memory. If this value is exceed requests to this - /// server will be denied with a 503 error code - /// </summary> - public int MaxAllowedSessions { get; init; } - } -}
\ No newline at end of file diff --git a/libs/VNLib.Plugins.Sessions.Memory/src/MemorySessionEntrypoint.cs b/libs/VNLib.Plugins.Sessions.Memory/src/MemorySessionEntrypoint.cs deleted file mode 100644 index 6cd25ab..0000000 --- a/libs/VNLib.Plugins.Sessions.Memory/src/MemorySessionEntrypoint.cs +++ /dev/null @@ -1,95 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Sessions.Memory -* File: MemorySessionEntrypoint.cs -* -* MemorySessionEntrypoint.cs is part of VNLib.Plugins.Essentials.Sessions.Memory which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Sessions.Memory is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Sessions.Memory is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; - -using VNLib.Net.Http; -using VNLib.Utils.Logging; -using VNLib.Utils.Extensions; -using VNLib.Plugins.Essentials.Sessions; -using VNLib.Plugins.Extensions.Loading.Events; -using VNLib.Plugins.Extensions.Loading; - -namespace VNLib.Plugins.Sessions.Memory -{ - - /// <summary> - /// Dynamically loadable session provider - /// </summary> - public sealed class MemorySessionEntrypoint : ISessionProvider, IIntervalScheduleable - { - const string WEB_SESSION_CONFIG = "web"; - - private MemorySessionStore? _sessions; - - public bool CanProcess(IHttpEvent entity) - { - //Web sessions can always be provided - return _sessions != null; - } - - public ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) - { - return _sessions!.GetSessionAsync(entity, cancellationToken); - } - - public void Load(PluginBase plugin, ILogProvider localized) - { - //Get websessions config element - - IReadOnlyDictionary<string, JsonElement> webSessionConfig = plugin.GetConfig(WEB_SESSION_CONFIG); - - MemorySessionConfig config = new() - { - SessionLog = localized, - MaxAllowedSessions = webSessionConfig["cache_size"].GetInt32(), - SessionIdSizeBytes = webSessionConfig["cookie_size"].GetUInt32(), - SessionTimeout = webSessionConfig["valid_for_sec"].GetTimeSpan(TimeParseType.Seconds), - SessionCookieID = webSessionConfig["cookie_name"].GetString() ?? throw new KeyNotFoundException($"Missing required element 'cookie_name' for config '{WEB_SESSION_CONFIG}'"), - }; - - _sessions = new(config); - - //Begin listening for expired records - _ = plugin.ObserveTask(() => _sessions.CleanupExiredAsync(localized, plugin.UnloadToken)); - - //Schedule garbage collector - plugin.ScheduleInterval(this, TimeSpan.FromMinutes(1)); - - //Call cleanup on exit - _ = plugin.RegisterForUnload(_sessions.Cleanup); - } - - Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken) - { - //Cleanup expired sessions on interval - _sessions?.GC(); - return Task.CompletedTask; - } - } -} diff --git a/libs/VNLib.Plugins.Sessions.Memory/src/MemorySessionStore.cs b/libs/VNLib.Plugins.Sessions.Memory/src/MemorySessionStore.cs deleted file mode 100644 index 774681a..0000000 --- a/libs/VNLib.Plugins.Sessions.Memory/src/MemorySessionStore.cs +++ /dev/null @@ -1,168 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Sessions.Memory -* File: MemorySessionStore.cs -* -* MemorySessionStore.cs is part of VNLib.Plugins.Essentials.Sessions.Memory which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Sessions.Memory is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Sessions.Memory is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; - -using VNLib.Net.Http; -using VNLib.Net.Http.Core; -using VNLib.Utils; -using VNLib.Utils.Async; -using VNLib.Utils.Logging; -using VNLib.Utils.Extensions; -using VNLib.Plugins.Essentials; -using VNLib.Plugins.Essentials.Sessions; -using VNLib.Plugins.Essentials.Extensions; - -namespace VNLib.Plugins.Sessions.Memory -{ - /// <summary> - /// An <see cref="ISessionProvider"/> for in-process-memory backed sessions - /// </summary> - internal sealed class MemorySessionStore : ISessionProvider - { - private readonly Dictionary<string, MemorySession> SessionsStore; - - internal readonly MemorySessionConfig Config; - internal readonly SessionIdFactory IdFactory; - internal readonly AsyncQueue<MemorySession> ExpiredSessions; - - public MemorySessionStore(MemorySessionConfig config) - { - Config = config; - SessionsStore = new(config.MaxAllowedSessions, StringComparer.Ordinal); - IdFactory = new(config.SessionIdSizeBytes, config.SessionCookieID, config.SessionTimeout); - ExpiredSessions = new(false, true); - } - - ///<inheritdoc/> - public async ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) - { - - static ValueTask SessionHandleClosedAsync(ISession session, IHttpEvent ev) - { - return (session as MemorySession)!.UpdateAndRelease(true, ev); - } - - //Try to get the id for the session - if (IdFactory.TryGetSessionId(entity, out string? sessionId)) - { - //Try to get the old record or evict it - ERRNO result = SessionsStore.TryGetOrEvictRecord(sessionId, out MemorySession? session); - if(result > 0) - { - //Valid, now wait for exclusive access - await session.WaitOneAsync(cancellationToken); - return new (session, SessionHandleClosedAsync); - } - else - { - //try to cleanup expired records - GC(); - //Make sure there is enough room to add a new session - if (SessionsStore.Count >= Config.MaxAllowedSessions) - { - entity.Server.SetNoCache(); - //Set 503 when full - entity.CloseResponse(System.Net.HttpStatusCode.ServiceUnavailable); - //Cannot service new session - return new(null, FileProcessArgs.VirtualSkip, null); - } - //Initialze a new session - session = new(sessionId, entity.Server.GetTrustedIp(), UpdateSessionId, ExpiredSessions); - //Increment the semaphore - (session as IWaitHandle).WaitOne(); - //store the session in cache while holding semaphore, and set its expiration - SessionsStore.StoreRecord(session.SessionID, session, Config.SessionTimeout); - //Init new session handle - return new (session, SessionHandleClosedAsync); - } - } - else - { - return SessionHandle.Empty; - } - } - - public async Task CleanupExiredAsync(ILogProvider log, CancellationToken token) - { - while (true) - { - try - { - //Wait for expired session and dispose it - using MemorySession session = await ExpiredSessions.DequeueAsync(token); - - //Obtain lock on session - await session.WaitOneAsync(CancellationToken.None); - - log.Verbose("Removed expired session {id}", session.SessionID); - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - log.Error(ex); - } - } - } - - private string UpdateSessionId(IHttpEvent entity, string oldId) - { - //Generate and set a new sessionid - string newid = IdFactory.GenerateSessionId(entity); - //Aquire lock on cache - lock (SessionsStore) - { - //Change the cache lookup id - if (SessionsStore.Remove(oldId, out MemorySession? session)) - { - SessionsStore.Add(newid, session); - } - } - return newid; - } - - /// <summary> - /// Evicts all sessions from the current store - /// </summary> - public void Cleanup() - { - //Expire all old records to cleanup all entires - this.SessionsStore.CollectRecords(DateTime.MaxValue); - } - /// <summary> - /// Collects all expired records from the current store - /// </summary> - public void GC() - { - //collect expired records - this.SessionsStore.CollectRecords(); - } - } -} diff --git a/libs/VNLib.Plugins.Sessions.Memory/src/SessionIdFactory.cs b/libs/VNLib.Plugins.Sessions.Memory/src/SessionIdFactory.cs deleted file mode 100644 index 253fa81..0000000 --- a/libs/VNLib.Plugins.Sessions.Memory/src/SessionIdFactory.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Sessions.Memory -* File: SessionIdFactory.cs -* -* SessionIdFactory.cs is part of VNLib.Plugins.Essentials.Sessions.Memory which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Sessions.Memory is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Sessions.Memory is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Diagnostics.CodeAnalysis; - -using VNLib.Hashing; -using VNLib.Net.Http; -using VNLib.Plugins.Essentials.Extensions; - -namespace VNLib.Plugins.Sessions.Memory -{ - internal sealed class SessionIdFactory - { - private readonly int IdSize; - private readonly string cookieName; - private readonly TimeSpan ValidFor; - - public SessionIdFactory(uint idSize, string cookieName, TimeSpan validFor) - { - IdSize = (int)idSize; - this.cookieName = cookieName; - ValidFor = validFor; - } - - public string GenerateSessionId(IHttpEvent entity) - { - //Random hex hash - string cookie = RandomHash.GetRandomBase32(IdSize); - - //Set the session id cookie - entity.Server.SetCookie(cookieName, cookie, ValidFor, secure: true, httpOnly: true); - - //return session-id value from cookie value - return cookie; - } - - public bool TryGetSessionId(IHttpEvent entity, [NotNullWhen(true)] out string? sessionId) - { - //Get authorization token and make sure its not too large to cause a buffer overflow - if (entity.Server.GetCookie(cookieName, out sessionId)) - { - return true; - } - //Only add sessions for user-agents - else if (entity.Server.IsBrowser()) - { - //Get a new session id - sessionId = GenerateSessionId(entity); - - return true; - } - else - { - sessionId = null; - return false; - } - } - } -} diff --git a/libs/VNLib.Plugins.Sessions.Memory/src/VNLib.Plugins.Sessions.Memory.csproj b/libs/VNLib.Plugins.Sessions.Memory/src/VNLib.Plugins.Sessions.Memory.csproj deleted file mode 100644 index cfffe28..0000000 --- a/libs/VNLib.Plugins.Sessions.Memory/src/VNLib.Plugins.Sessions.Memory.csproj +++ /dev/null @@ -1,49 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <PropertyGroup> - <Nullable>enable</Nullable> - <TargetFramework>net6.0</TargetFramework> - <AssemblyName>VNLib.Plugins.Sessions.Memory</AssemblyName> - <RootNamespace>VNLib.Plugins.Sessions.Memory</RootNamespace> - <SignAssembly>True</SignAssembly> - <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile> - <GenerateDocumentationFile>True</GenerateDocumentationFile> - <AnalysisLevel>latest-all</AnalysisLevel> - <EnableDynamicLoading>true</EnableDynamicLoading> - </PropertyGroup> - - <PropertyGroup> - <Authors>Vaughn Nugent</Authors> - <Copyright>Copyright © 2023 Vaughn Nugent</Copyright> - <PackageProjectUrl>https://www.vaughnugent.com/resources/software</PackageProjectUrl> - </PropertyGroup> - - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> - <Deterministic>False</Deterministic> - </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> - <Deterministic>False</Deterministic> - </PropertyGroup> - - <ProjectExtensions><VisualStudio><UserProperties /></VisualStudio></ProjectExtensions> - <ItemGroup> - <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - </PackageReference> - <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - </PackageReference> - </ItemGroup> - <ItemGroup> - <ProjectReference Include="..\..\..\..\..\core\lib\Plugins.Essentials\src\VNLib.Plugins.Essentials.csproj" /> - <ProjectReference Include="..\..\..\..\..\core\lib\Utils\src\VNLib.Utils.csproj" /> - <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" /> - </ItemGroup> - - <Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent"> - <Exec Command="start xcopy "$(TargetDir)" "..\..\..\..\..\devplugins\RuntimeAssets\$(TargetName)" /E /Y /R" /> - </Target> - -</Project> |