From 9d0300719c69645f40a7a3f230e43a8d26a12364 Mon Sep 17 00:00:00 2001 From: Philip Kelley Date: Mon, 7 Oct 2013 16:41:16 -0400 Subject: [PATCH 1/3] Subtransport logic. --- LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj | 1 + .../SmartSubtransportFixture.cs | 264 ++++++++++++++++++ LibGit2Sharp/Core/GitSmartSubtransport.cs | 36 +++ .../Core/GitSmartSubtransportRegistration.cs | 16 ++ .../Core/GitSmartSubtransportStream.cs | 42 +++ LibGit2Sharp/Core/NativeMethods.cs | 20 ++ LibGit2Sharp/Core/Proxy.cs | 14 + LibGit2Sharp/LibGit2.cs | 59 ++++ LibGit2Sharp/LibGit2Sharp.csproj | 7 + LibGit2Sharp/SmartSubtransport.cs | 184 ++++++++++++ LibGit2Sharp/SmartSubtransportRegistration.cs | 136 +++++++++ LibGit2Sharp/SmartSubtransportStream.cs | 190 +++++++++++++ 12 files changed, 969 insertions(+) create mode 100644 LibGit2Sharp.Tests/SmartSubtransportFixture.cs create mode 100644 LibGit2Sharp/Core/GitSmartSubtransport.cs create mode 100644 LibGit2Sharp/Core/GitSmartSubtransportRegistration.cs create mode 100644 LibGit2Sharp/Core/GitSmartSubtransportStream.cs create mode 100644 LibGit2Sharp/LibGit2.cs create mode 100644 LibGit2Sharp/SmartSubtransport.cs create mode 100644 LibGit2Sharp/SmartSubtransportRegistration.cs create mode 100644 LibGit2Sharp/SmartSubtransportStream.cs diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj index 76d100e60..739b42a00 100644 --- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj +++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj @@ -99,6 +99,7 @@ + diff --git a/LibGit2Sharp.Tests/SmartSubtransportFixture.cs b/LibGit2Sharp.Tests/SmartSubtransportFixture.cs new file mode 100644 index 000000000..6eba2a05f --- /dev/null +++ b/LibGit2Sharp.Tests/SmartSubtransportFixture.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; +using Xunit.Extensions; + +namespace LibGit2Sharp.Tests +{ + public class SmartSubtransportFixture : BaseFixture + { + [Theory] + [InlineData("http://github.com/libgit2/TestGitRepository")] + [InlineData("https://github.com/libgit2/TestGitRepository")] + public void CustomSmartSubtransportTest(string url) + { + string remoteName = "testRemote"; + + var scd = BuildSelfCleaningDirectory(); + var repoPath = Repository.Init(scd.RootedDirectoryPath); + + SmartSubtransportRegistration httpRegistration = null; + SmartSubtransportRegistration httpsRegistration = null; + + try + { + httpRegistration = LibGit2.RegisterSmartSubtransport("http://", 2); + httpsRegistration = LibGit2.RegisterSmartSubtransport("https://", 2); + + using (var repo = new Repository(scd.DirectoryPath)) + { + Remote remote = repo.Network.Remotes.Add(remoteName, url); + + // Set up structures for the expected results + // and verifying the RemoteUpdateTips callback. + TestRemoteInfo expectedResults = TestRemoteInfo.TestRemoteInstance; + ExpectedFetchState expectedFetchState = new ExpectedFetchState(remoteName); + + // Add expected branch objects + foreach (KeyValuePair kvp in expectedResults.BranchTips) + { + expectedFetchState.AddExpectedBranch(kvp.Key, ObjectId.Zero, kvp.Value); + } + + // Add the expected tags + string[] expectedTagNames = { "blob", "commit_tree" }; + foreach (string tagName in expectedTagNames) + { + TestRemoteInfo.ExpectedTagInfo expectedTagInfo = expectedResults.Tags[tagName]; + expectedFetchState.AddExpectedTag(tagName, ObjectId.Zero, expectedTagInfo); + } + + // Perform the actual fetch + repo.Network.Fetch(remote, new FetchOptions { OnUpdateTips = expectedFetchState.RemoteUpdateTipsHandler, TagFetchMode = TagFetchMode.Auto }); + + // Verify the expected + expectedFetchState.CheckUpdatedReferences(repo); + } + } + finally + { + if (httpRegistration != null) + { + LibGit2.UnregisterSmartSubtransport(httpRegistration); + } + + if (httpsRegistration != null) + { + LibGit2.UnregisterSmartSubtransport(httpsRegistration); + } + } + } + + [RpcSmartSubtransport] + private class MockSmartSubtransport : SmartSubtransport + { + protected override SmartSubtransportStream Action(String url, GitSmartSubtransportAction action) + { + String endpointUrl, contentType = null; + bool isPost = false; + + switch (action) + { + case GitSmartSubtransportAction.UploadPackList: + endpointUrl = String.Concat(url, "/info/refs?service=git-upload-pack"); + break; + + case GitSmartSubtransportAction.UploadPack: + endpointUrl = String.Concat(url, "/git-upload-pack"); + contentType = "application/x-git-upload-pack-request"; + isPost = true; + break; + + case GitSmartSubtransportAction.ReceivePackList: + endpointUrl = String.Concat(url, "/info/refs?service=git-receive-pack"); + break; + + case GitSmartSubtransportAction.ReceivePack: + endpointUrl = String.Concat(url, "/git-receive-pack"); + contentType = "application/x-git-receive-pack-request"; + isPost = true; + break; + + default: + throw new InvalidOperationException(); + } + + return new MockSmartSubtransportStream(this, endpointUrl, isPost, contentType); + } + + private class MockSmartSubtransportStream : SmartSubtransportStream + { + private static int MAX_REDIRECTS = 5; + + private MemoryStream postBuffer = new MemoryStream(); + private Stream responseStream; + + public MockSmartSubtransportStream(MockSmartSubtransport parent, string endpointUrl, bool isPost, string contentType) + : base(parent) + { + EndpointUrl = endpointUrl; + IsPost = isPost; + ContentType = contentType; + } + + private string EndpointUrl + { + get; + set; + } + + private bool IsPost + { + get; + set; + } + + private string ContentType + { + get; + set; + } + + public override int Write(Stream dataStream, long length) + { + byte[] buffer = new byte[4096]; + long writeTotal = 0; + + while (length > 0) + { + int readLen = dataStream.Read(buffer, 0, (int)Math.Min(buffer.Length, length)); + + if (readLen == 0) + { + break; + } + + postBuffer.Write(buffer, 0, readLen); + length -= readLen; + writeTotal += readLen; + } + + if (writeTotal < length) + { + throw new EndOfStreamException("Could not write buffer (short read)"); + } + + return 0; + } + + private static HttpWebRequest CreateWebRequest(string endpointUrl, bool isPost, string contentType) + { + HttpWebRequest webRequest = (HttpWebRequest)HttpWebRequest.Create(endpointUrl); + webRequest.UserAgent = "git/1.0 (libgit2 custom transport)"; + webRequest.ServicePoint.Expect100Continue = false; + webRequest.AllowAutoRedirect = false; + + if (isPost) + { + webRequest.Method = "POST"; + webRequest.ContentType = contentType; + } + + return webRequest; + } + + private HttpWebResponse GetResponseWithRedirects() + { + HttpWebRequest request = CreateWebRequest(EndpointUrl, IsPost, ContentType); + HttpWebResponse response = null; + + for (int i = 0; i < MAX_REDIRECTS; i++) + { + if (IsPost && postBuffer.Length > 0) + { + postBuffer.Seek(0, SeekOrigin.Begin); + + using (Stream requestStream = request.GetRequestStream()) + { + postBuffer.WriteTo(requestStream); + } + } + + response = (HttpWebResponse)request.GetResponse(); + + if (response.StatusCode == HttpStatusCode.Moved || response.StatusCode == HttpStatusCode.Redirect) + { + request = CreateWebRequest(response.Headers["Location"], IsPost, ContentType); + continue; + } + + break; + } + + if (response == null) + { + throw new Exception("Too many redirects"); + } + + return response; + } + + public override int Read(Stream dataStream, long length, out long readTotal) + { + byte[] buffer = new byte[4096]; + readTotal = 0; + + if (responseStream == null) + { + HttpWebResponse response = GetResponseWithRedirects(); + responseStream = response.GetResponseStream(); + } + + while (length > 0) + { + int readLen = responseStream.Read(buffer, 0, (int)Math.Min(buffer.Length, length)); + + if (readLen == 0) + break; + + dataStream.Write(buffer, 0, readLen); + readTotal += readLen; + length -= readLen; + } + + return 0; + } + + protected override void Dispose() + { + if (responseStream != null) + { + responseStream.Dispose(); + responseStream = null; + } + + base.Dispose(); + } + } + } + } +} diff --git a/LibGit2Sharp/Core/GitSmartSubtransport.cs b/LibGit2Sharp/Core/GitSmartSubtransport.cs new file mode 100644 index 000000000..d9b3c7545 --- /dev/null +++ b/LibGit2Sharp/Core/GitSmartSubtransport.cs @@ -0,0 +1,36 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core +{ + [StructLayout(LayoutKind.Sequential)] + internal class GitSmartSubtransport + { + static GitSmartSubtransport() + { + GCHandleOffset = Marshal.OffsetOf(typeof(GitSmartSubtransport), "GCHandle").ToInt32(); + } + + public action_callback Action; + public close_callback Close; + public free_callback Free; + + /* The libgit2 structure definition ends here. Subsequent fields are for libgit2sharp bookkeeping. */ + + public IntPtr GCHandle; + + /* The following static fields are not part of the structure definition. */ + + public static int GCHandleOffset; + + public delegate int action_callback( + out IntPtr stream, + IntPtr subtransport, + IntPtr url, + GitSmartSubtransportAction action); + + public delegate int close_callback(IntPtr subtransport); + + public delegate void free_callback(IntPtr subtransport); + } +} diff --git a/LibGit2Sharp/Core/GitSmartSubtransportRegistration.cs b/LibGit2Sharp/Core/GitSmartSubtransportRegistration.cs new file mode 100644 index 000000000..724c6c414 --- /dev/null +++ b/LibGit2Sharp/Core/GitSmartSubtransportRegistration.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core +{ + [StructLayout(LayoutKind.Sequential)] + internal class GitSmartSubtransportRegistration + { + public IntPtr SubtransportCallback; + public uint Rpc; + + public delegate int create_callback( + out IntPtr subtransport, + IntPtr transport); + } +} diff --git a/LibGit2Sharp/Core/GitSmartSubtransportStream.cs b/LibGit2Sharp/Core/GitSmartSubtransportStream.cs new file mode 100644 index 000000000..f73218c09 --- /dev/null +++ b/LibGit2Sharp/Core/GitSmartSubtransportStream.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core +{ + [StructLayout(LayoutKind.Sequential)] + internal class GitSmartSubtransportStream + { + static GitSmartSubtransportStream() + { + GCHandleOffset = Marshal.OffsetOf(typeof(GitSmartSubtransportStream), "GCHandle").ToInt32(); + } + + public IntPtr SmartTransport; + + public read_callback Read; + public write_callback Write; + public free_callback Free; + + /* The libgit2 structure definition ends here. Subsequent fields are for libgit2sharp bookkeeping. */ + + public IntPtr GCHandle; + + /* The following static fields are not part of the structure definition. */ + + public static int GCHandleOffset; + + public delegate int read_callback( + IntPtr stream, + IntPtr buffer, + UIntPtr buf_size, + out UIntPtr bytes_read); + + public delegate int write_callback( + IntPtr stream, + IntPtr buffer, + UIntPtr len); + + public delegate void free_callback( + IntPtr stream); + } +} diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 0653503bd..40214441f 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -1396,6 +1396,26 @@ internal static extern int git_tag_delete( internal delegate int git_transfer_progress_callback(ref GitTransferProgress stats, IntPtr payload); + internal delegate int git_transport_cb(out IntPtr transport, IntPtr remote, IntPtr payload); + + [DllImport(libgit2)] + internal static extern int git_transport_register( + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string prefix, + uint priority, + IntPtr transport_cb, + IntPtr payload); + + [DllImport(libgit2)] + internal static extern int git_transport_smart( + out IntPtr transport, + IntPtr remote, + IntPtr definition); + + [DllImport(libgit2)] + internal static extern void git_transport_unregister( + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string prefix, + uint priority); + [DllImport(libgit2)] internal static extern uint git_tree_entry_filemode(SafeHandle entry); diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index e698d971b..ba970461d 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -2620,6 +2620,20 @@ public static GitObjectType git_tag_target_type(GitObjectSafeHandle tag) #endregion + #region git_transport_ + + public static void git_transport_register(String prefix, uint priority, IntPtr transport_cb, IntPtr param) + { + Ensure.ZeroResult(NativeMethods.git_transport_register(prefix, priority, transport_cb, param)); + } + + public static void git_transport_unregister(String prefix, uint priority) + { + NativeMethods.git_transport_unregister(prefix, priority); + } + + #endregion + #region git_tree_ public static Mode git_tree_entry_attributes(SafeHandle entry) diff --git a/LibGit2Sharp/LibGit2.cs b/LibGit2Sharp/LibGit2.cs new file mode 100644 index 000000000..005918e2c --- /dev/null +++ b/LibGit2Sharp/LibGit2.cs @@ -0,0 +1,59 @@ +using System; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// Methods that affect the entirety of the Git implementation in libgit2 and + /// LibGit2Sharp. + /// + public sealed class LibGit2 + { + /// + /// Registers a new as a custom + /// smart-protocol transport with libgit2. + /// + /// The type of SmartSubtransport to register + /// The prefix (i.e. "http://") to register + /// The priority of the transport; the value must be 2 or larger to override a built-in transport + public static SmartSubtransportRegistration RegisterSmartSubtransport(string prefix, int priority) + where T : SmartSubtransport, new() + { + Ensure.ArgumentNotNull(prefix, "prefix"); + Ensure.ArgumentConformsTo(priority, s => s >= 0, "priority"); + + var registration = new SmartSubtransportRegistration(prefix, priority); + + try + { + Proxy.git_transport_register( + registration.Prefix, + (uint)registration.Priority, + registration.FunctionPointer, + registration.RegistrationPointer); + } + catch(Exception) + { + UnregisterSmartSubtransport(registration); + throw; + } + + return registration; + } + + /// + /// Unregisters a previously registered + /// as a custom smart-protocol transport with libgit2. + /// + /// The type of SmartSubtransport to register + /// The previous registration + public static void UnregisterSmartSubtransport(SmartSubtransportRegistration registration) + where T : SmartSubtransport, new() + { + Ensure.ArgumentNotNull(registration, "registration"); + + Proxy.git_transport_unregister(registration.Prefix, (uint)registration.Priority); + registration.Free(); + } + } +} diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index e3c3ba0f7..fd5ee54f0 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -84,6 +84,7 @@ + @@ -173,6 +174,7 @@ + @@ -200,6 +202,11 @@ + + + + + diff --git a/LibGit2Sharp/SmartSubtransport.cs b/LibGit2Sharp/SmartSubtransport.cs new file mode 100644 index 000000000..6395139d8 --- /dev/null +++ b/LibGit2Sharp/SmartSubtransport.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// When applied to a subclass of SmartSubtransport, indicates that this + /// custom subtransport has RPC semantics (request/response). HTTP falls into + /// this category; the git:// protocol, which uses a raw socket, does not. + /// + [AttributeUsage(AttributeTargets.Class)] + public class RpcSmartSubtransportAttribute : Attribute + { + } + + /// + /// An enumeration of the type of connections which a "smart" subtransport + /// may be asked to create on behalf of libgit2. + /// + public enum GitSmartSubtransportAction + { + /// + /// For HTTP, this indicates a GET to /info/refs?service=git-upload-pack + /// + UploadPackList = 1, + + /// + /// For HTTP, this indicates a POST to /git-upload-pack + /// + UploadPack = 2, + + /// + /// For HTTP, this indicates a GET to /info/refs?service=git-receive-pack + /// + ReceivePackList = 3, + + /// + /// For HTTP, this indicates a POST to /git-receive-pack + /// + ReceivePack = 4 + } + + /// + /// Base class for custom subtransports for the "smart" transport. + /// + public abstract class SmartSubtransport + { + /// + /// Invoked by libgit2 to create a connection using this subtransport. + /// + /// The endpoint to connect to + /// The type of connection to create + /// A SmartSubtransportStream representing the connection + protected abstract SmartSubtransportStream Action(String url, GitSmartSubtransportAction action); + + /// + /// Invoked by libgit2 when this subtransport is no longer needed, but may be re-used in the future. + /// Override this method to add additional cleanup steps to your subclass. Be sure to call base.Close(). + /// + protected virtual void Close() + { + } + + /// + /// Invoked by libgit2 when this subtransport is being freed. Override this method to add additional + /// cleanup steps to your subclass. Be sure to call base.Dispose(). + /// + protected virtual void Dispose() + { + Close(); + + if (IntPtr.Zero != nativeSubtransportPointer) + { + GCHandle.FromIntPtr(Marshal.ReadIntPtr(nativeSubtransportPointer, GitSmartSubtransport.GCHandleOffset)).Free(); + Marshal.FreeHGlobal(nativeSubtransportPointer); + nativeSubtransportPointer = IntPtr.Zero; + } + } + + private IntPtr nativeSubtransportPointer; + + internal IntPtr GitSmartSubtransportPointer + { + get + { + if (IntPtr.Zero == nativeSubtransportPointer) + { + var nativeTransport = new GitSmartSubtransport(); + + nativeTransport.Action = EntryPoints.ActionCallback; + nativeTransport.Close = EntryPoints.CloseCallback; + nativeTransport.Free = EntryPoints.FreeCallback; + + nativeTransport.GCHandle = GCHandle.ToIntPtr(GCHandle.Alloc(this)); + nativeSubtransportPointer = Marshal.AllocHGlobal(Marshal.SizeOf(nativeTransport)); + Marshal.StructureToPtr(nativeTransport, nativeSubtransportPointer, false); + } + + return nativeSubtransportPointer; + } + } + + private static class EntryPoints + { + // Because our GitSmartSubtransport structure exists on the managed heap only for a short time (to be marshaled + // to native memory with StructureToPtr), we need to bind to static delegates. If at construction time + // we were to bind to the methods directly, that's the same as newing up a fresh delegate every time. + // Those delegates won't be rooted in the object graph and can be collected as soon as StructureToPtr finishes. + public static GitSmartSubtransport.action_callback ActionCallback = new GitSmartSubtransport.action_callback(Action); + public static GitSmartSubtransport.close_callback CloseCallback = new GitSmartSubtransport.close_callback(Close); + public static GitSmartSubtransport.free_callback FreeCallback = new GitSmartSubtransport.free_callback(Free); + + private static int Action( + out IntPtr stream, + IntPtr subtransport, + IntPtr url, + GitSmartSubtransportAction action) + { + stream = IntPtr.Zero; + + SmartSubtransport t = GCHandle.FromIntPtr(Marshal.ReadIntPtr(subtransport, GitSmartSubtransport.GCHandleOffset)).Target as SmartSubtransport; + String urlAsString = LaxUtf8Marshaler.FromNative(url); + + if (null != t && + !String.IsNullOrEmpty(urlAsString)) + { + try + { + stream = t.Action(urlAsString, action).GitSmartTransportStreamPointer; + + return 0; + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Net, ex); + } + } + + return (int)GitErrorCode.Error; + } + + private static int Close(IntPtr subtransport) + { + SmartSubtransport t = GCHandle.FromIntPtr(Marshal.ReadIntPtr(subtransport, GitSmartSubtransport.GCHandleOffset)).Target as SmartSubtransport; + + if (null != t) + { + try + { + t.Close(); + + return 0; + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Net, ex); + } + } + + return (int)GitErrorCode.Error; + } + + private static void Free(IntPtr subtransport) + { + SmartSubtransport t = GCHandle.FromIntPtr(Marshal.ReadIntPtr(subtransport, GitSmartSubtransport.GCHandleOffset)).Target as SmartSubtransport; + + if (null != t) + { + try + { + t.Dispose(); + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Net, ex); + } + } + } + } + } +} diff --git a/LibGit2Sharp/SmartSubtransportRegistration.cs b/LibGit2Sharp/SmartSubtransportRegistration.cs new file mode 100644 index 000000000..6938e89e4 --- /dev/null +++ b/LibGit2Sharp/SmartSubtransportRegistration.cs @@ -0,0 +1,136 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// An object representing the registration of a SmartSubtransport type with libgit2 + /// under a particular prefix (i.e. "http://") and priority. + /// + /// The type of SmartSubtransport to register + public sealed class SmartSubtransportRegistration + where T : SmartSubtransport, new() + { + /// + /// Creates a new native registration for a smart protocol transport + /// in libgit2. + /// + /// The prefix (i.e. "http://") to register + /// The priority of the transport; the value must be 2 or larger to override a built-in transport + internal SmartSubtransportRegistration(string prefix, int priority) + { + Prefix = prefix; + Priority = priority; + RegistrationPointer = CreateRegistrationPointer(); + FunctionPointer = CreateFunctionPointer(); + } + + /// + /// The URI prefix (ie "http://") for this transport. + /// + public string Prefix + { + get; + private set; + } + + /// + /// The priority of this transport relative to others + /// + public int Priority + { + get; + private set; + } + + internal IntPtr RegistrationPointer + { + get; + private set; + } + + internal IntPtr FunctionPointer + { + get; + private set; + } + + private IntPtr CreateRegistrationPointer() + { + var registration = new GitSmartSubtransportRegistration(); + + registration.SubtransportCallback = Marshal.GetFunctionPointerForDelegate(EntryPoints.SubtransportCallback); + + if (typeof(T).GetCustomAttributes(true).Where(s => s.GetType() == typeof(RpcSmartSubtransportAttribute)).FirstOrDefault() != null) + { + registration.Rpc = 1; + } + + var registrationPointer = Marshal.AllocHGlobal(Marshal.SizeOf(registration)); + Marshal.StructureToPtr(registration, registrationPointer, false); + + return registrationPointer; + } + + private IntPtr CreateFunctionPointer() + { + return Marshal.GetFunctionPointerForDelegate(EntryPoints.TransportCallback); + } + + internal void Free() + { + Marshal.FreeHGlobal(RegistrationPointer); + } + + private static class EntryPoints + { + // Because our GitSmartSubtransportRegistration structure exists on the managed heap only for a short time (to be marshaled + // to native memory with StructureToPtr), we need to bind to static delegates. If at construction time + // we were to bind to the methods directly, that's the same as newing up a fresh delegate every time. + // Those delegates won't be rooted in the object graph and can be collected as soon as StructureToPtr finishes. + public static GitSmartSubtransportRegistration.create_callback SubtransportCallback = new GitSmartSubtransportRegistration.create_callback(Subtransport); + public static NativeMethods.git_transport_cb TransportCallback = new NativeMethods.git_transport_cb(Transport); + + private static int Subtransport( + out IntPtr subtransport, + IntPtr transport) + { + subtransport = IntPtr.Zero; + + try + { + subtransport = new T().GitSmartSubtransportPointer; + + return 0; + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Net, ex); + } + + return (int)GitErrorCode.Error; + } + + private static int Transport( + out IntPtr transport, + IntPtr remote, + IntPtr payload) + { + transport = IntPtr.Zero; + + try + { + return NativeMethods.git_transport_smart(out transport, remote, payload); + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Net, ex); + } + + return (int)GitErrorCode.Error; + } + } + } +} diff --git a/LibGit2Sharp/SmartSubtransportStream.cs b/LibGit2Sharp/SmartSubtransportStream.cs new file mode 100644 index 000000000..6cd24998d --- /dev/null +++ b/LibGit2Sharp/SmartSubtransportStream.cs @@ -0,0 +1,190 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// A stream that represents a two-way connection (socket) for a SmartSubtransport. + /// + public abstract class SmartSubtransportStream + { + /// + /// This is to quiet the MetaFixture.TypesInLibGit2SharpMustBeExtensibleInATestingContext test. + /// Do not use this constructor. + /// + protected internal SmartSubtransportStream() + { + throw new InvalidOperationException(); + } + + /// + /// Base constructor for SmartTransportStream. Make sure that your derived class calls this base constructor. + /// + /// The subtransport that this stream represents a connection over. + protected SmartSubtransportStream(SmartSubtransport subtransport) + { + this.subtransport = subtransport; + } + + /// + /// Invoked by libgit2 when this stream is no longer needed. + /// Override this method to add additional cleanup steps to your subclass. Be sure + /// to call base.Dispose(). + /// + protected virtual void Dispose() + { + if (IntPtr.Zero != nativeStreamPointer) + { + GCHandle.FromIntPtr(Marshal.ReadIntPtr(nativeStreamPointer, GitSmartSubtransportStream.GCHandleOffset)).Free(); + Marshal.FreeHGlobal(nativeStreamPointer); + nativeStreamPointer = IntPtr.Zero; + } + } + + /// + /// Requests that the stream write the next length bytes of the stream to the provided Stream object. + /// + public abstract int Read( + Stream dataStream, + long length, + out long bytesRead); + + /// + /// Requests that the stream write the first length bytes of the provided Stream object to the stream. + /// + public abstract int Write( + Stream dataStream, + long length); + + /// + /// The smart transport that this stream represents a connection over. + /// + public virtual SmartSubtransport SmartTransport + { + get + { + return this.subtransport; + } + } + + private SmartSubtransport subtransport; + private IntPtr nativeStreamPointer; + + internal IntPtr GitSmartTransportStreamPointer + { + get + { + if (IntPtr.Zero == nativeStreamPointer) + { + var nativeTransportStream = new GitSmartSubtransportStream(); + + nativeTransportStream.SmartTransport = this.subtransport.GitSmartSubtransportPointer; + nativeTransportStream.Read = EntryPoints.ReadCallback; + nativeTransportStream.Write = EntryPoints.WriteCallback; + nativeTransportStream.Free = EntryPoints.FreeCallback; + + nativeTransportStream.GCHandle = GCHandle.ToIntPtr(GCHandle.Alloc(this)); + nativeStreamPointer = Marshal.AllocHGlobal(Marshal.SizeOf(nativeTransportStream)); + Marshal.StructureToPtr(nativeTransportStream, nativeStreamPointer, false); + } + + return nativeStreamPointer; + } + } + + private static class EntryPoints + { + // Because our GitSmartSubtransportStream structure exists on the managed heap only for a short time (to be marshaled + // to native memory with StructureToPtr), we need to bind to static delegates. If at construction time + // we were to bind to the methods directly, that's the same as newing up a fresh delegate every time. + // Those delegates won't be rooted in the object graph and can be collected as soon as StructureToPtr finishes. + public static GitSmartSubtransportStream.read_callback ReadCallback = new GitSmartSubtransportStream.read_callback(Read); + public static GitSmartSubtransportStream.write_callback WriteCallback = new GitSmartSubtransportStream.write_callback(Write); + public static GitSmartSubtransportStream.free_callback FreeCallback = new GitSmartSubtransportStream.free_callback(Free); + + private unsafe static int Read( + IntPtr stream, + IntPtr buffer, + UIntPtr buf_size, + out UIntPtr bytes_read) + { + bytes_read = UIntPtr.Zero; + + SmartSubtransportStream transportStream = GCHandle.FromIntPtr(Marshal.ReadIntPtr(stream, GitSmartSubtransportStream.GCHandleOffset)).Target as SmartSubtransportStream; + + if (transportStream != null && + buf_size.ToUInt64() < (ulong)long.MaxValue) + { + using (UnmanagedMemoryStream memoryStream = new UnmanagedMemoryStream((byte*)buffer, 0, (long)buf_size.ToUInt64(), FileAccess.ReadWrite)) + { + try + { + long longBytesRead; + + int toReturn = transportStream.Read(memoryStream, (long)buf_size.ToUInt64(), out longBytesRead); + + bytes_read = new UIntPtr((ulong)Math.Max(0, longBytesRead)); + + return toReturn; + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Net, ex); + } + } + } + + return (int)GitErrorCode.Error; + } + + private static unsafe int Write( + IntPtr stream, + IntPtr buffer, + UIntPtr len) + { + SmartSubtransportStream transportStream = GCHandle.FromIntPtr(Marshal.ReadIntPtr(stream, GitSmartSubtransportStream.GCHandleOffset)).Target as SmartSubtransportStream; + + if (transportStream != null && + len.ToUInt64() < (ulong)long.MaxValue) + { + long length = (long)len.ToUInt64(); + + using (UnmanagedMemoryStream dataStream = new UnmanagedMemoryStream((byte*)buffer, length)) + { + try + { + return transportStream.Write(dataStream, length); + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Net, ex); + } + } + } + + return (int)GitErrorCode.Error; + } + + private static void Free( + IntPtr stream) + { + SmartSubtransportStream transportStream = GCHandle.FromIntPtr(Marshal.ReadIntPtr(stream, GitSmartSubtransportStream.GCHandleOffset)).Target as SmartSubtransportStream; + + if (transportStream != null) + { + try + { + transportStream.Dispose(); + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Net, ex); + } + } + } + } + } +} + From 86685a3e6cd9a4a1018fd86358dbf63eb14f3b3d Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Fri, 18 Apr 2014 20:29:31 -0500 Subject: [PATCH 2/3] R# all the things --- LibGit2Sharp/SmartSubtransportRegistration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LibGit2Sharp/SmartSubtransportRegistration.cs b/LibGit2Sharp/SmartSubtransportRegistration.cs index 6938e89e4..4e8fa52cb 100644 --- a/LibGit2Sharp/SmartSubtransportRegistration.cs +++ b/LibGit2Sharp/SmartSubtransportRegistration.cs @@ -63,7 +63,7 @@ private IntPtr CreateRegistrationPointer() registration.SubtransportCallback = Marshal.GetFunctionPointerForDelegate(EntryPoints.SubtransportCallback); - if (typeof(T).GetCustomAttributes(true).Where(s => s.GetType() == typeof(RpcSmartSubtransportAttribute)).FirstOrDefault() != null) + if (typeof(T).GetCustomAttributes(true).Any(s => s.GetType() == typeof(RpcSmartSubtransportAttribute))) { registration.Rpc = 1; } From 6f0aef4ee7459eb7a9882560ba22fd7d38bcc352 Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Fri, 18 Apr 2014 20:31:58 -0500 Subject: [PATCH 3/3] Make SmartSubtransportRegistration mockable --- .../SmartSubtransportFixture.cs | 29 +++++++-------- LibGit2Sharp/LibGit2.cs | 36 +++++++++---------- LibGit2Sharp/SmartSubtransportRegistration.cs | 23 ++++++++---- 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/LibGit2Sharp.Tests/SmartSubtransportFixture.cs b/LibGit2Sharp.Tests/SmartSubtransportFixture.cs index 6eba2a05f..feecee9cc 100644 --- a/LibGit2Sharp.Tests/SmartSubtransportFixture.cs +++ b/LibGit2Sharp.Tests/SmartSubtransportFixture.cs @@ -21,14 +21,9 @@ public void CustomSmartSubtransportTest(string url) var scd = BuildSelfCleaningDirectory(); var repoPath = Repository.Init(scd.RootedDirectoryPath); - SmartSubtransportRegistration httpRegistration = null; - SmartSubtransportRegistration httpsRegistration = null; - - try + using ((IDisposable)LibGit2.RegisterSmartSubtransport(new MockSmartSubtransportRegistration("http://", 2))) + using ((IDisposable)LibGit2.RegisterSmartSubtransport(new MockSmartSubtransportRegistration("https://", 2))) { - httpRegistration = LibGit2.RegisterSmartSubtransport("http://", 2); - httpsRegistration = LibGit2.RegisterSmartSubtransport("https://", 2); - using (var repo = new Repository(scd.DirectoryPath)) { Remote remote = repo.Network.Remotes.Add(remoteName, url); @@ -59,17 +54,19 @@ public void CustomSmartSubtransportTest(string url) expectedFetchState.CheckUpdatedReferences(repo); } } - finally + } + + private class MockSmartSubtransportRegistration : SmartSubtransportRegistration, IDisposable + { + public MockSmartSubtransportRegistration(string prefix, int priority) { - if (httpRegistration != null) - { - LibGit2.UnregisterSmartSubtransport(httpRegistration); - } + Prefix = prefix; + Priority = priority; + } - if (httpsRegistration != null) - { - LibGit2.UnregisterSmartSubtransport(httpsRegistration); - } + public void Dispose() + { + Free(); } } diff --git a/LibGit2Sharp/LibGit2.cs b/LibGit2Sharp/LibGit2.cs index 005918e2c..07e3d5700 100644 --- a/LibGit2Sharp/LibGit2.cs +++ b/LibGit2Sharp/LibGit2.cs @@ -19,11 +19,22 @@ public sealed class LibGit2 public static SmartSubtransportRegistration RegisterSmartSubtransport(string prefix, int priority) where T : SmartSubtransport, new() { - Ensure.ArgumentNotNull(prefix, "prefix"); - Ensure.ArgumentConformsTo(priority, s => s >= 0, "priority"); - var registration = new SmartSubtransportRegistration(prefix, priority); + return RegisterSmartSubtransport(registration); + } + + /// + /// + /// + /// + /// + /// + public static SmartSubtransportRegistration RegisterSmartSubtransport(SmartSubtransportRegistration registration) + where T : SmartSubtransport, new() + { + Ensure.ArgumentNotNull(registration, "registration"); + try { Proxy.git_transport_register( @@ -32,28 +43,13 @@ public static SmartSubtransportRegistration RegisterSmartSubtransport(stri registration.FunctionPointer, registration.RegistrationPointer); } - catch(Exception) + catch (Exception) { - UnregisterSmartSubtransport(registration); + registration.Free(); throw; } return registration; } - - /// - /// Unregisters a previously registered - /// as a custom smart-protocol transport with libgit2. - /// - /// The type of SmartSubtransport to register - /// The previous registration - public static void UnregisterSmartSubtransport(SmartSubtransportRegistration registration) - where T : SmartSubtransport, new() - { - Ensure.ArgumentNotNull(registration, "registration"); - - Proxy.git_transport_unregister(registration.Prefix, (uint)registration.Priority); - registration.Free(); - } } } diff --git a/LibGit2Sharp/SmartSubtransportRegistration.cs b/LibGit2Sharp/SmartSubtransportRegistration.cs index 4e8fa52cb..4e34ba2dc 100644 --- a/LibGit2Sharp/SmartSubtransportRegistration.cs +++ b/LibGit2Sharp/SmartSubtransportRegistration.cs @@ -10,9 +10,16 @@ namespace LibGit2Sharp /// under a particular prefix (i.e. "http://") and priority. /// /// The type of SmartSubtransport to register - public sealed class SmartSubtransportRegistration + public class SmartSubtransportRegistration where T : SmartSubtransport, new() { + /// + /// Needed for mocking purposes. + /// + protected SmartSubtransportRegistration() + { + } + /// /// Creates a new native registration for a smart protocol transport /// in libgit2. @@ -21,6 +28,9 @@ public sealed class SmartSubtransportRegistration /// The priority of the transport; the value must be 2 or larger to override a built-in transport internal SmartSubtransportRegistration(string prefix, int priority) { + Ensure.ArgumentNotNull(prefix, "prefix"); + Ensure.ArgumentConformsTo(priority, s => s >= 0, "priority"); + Prefix = prefix; Priority = priority; RegistrationPointer = CreateRegistrationPointer(); @@ -30,19 +40,19 @@ internal SmartSubtransportRegistration(string prefix, int priority) /// /// The URI prefix (ie "http://") for this transport. /// - public string Prefix + public virtual string Prefix { get; - private set; + protected set; } /// /// The priority of this transport relative to others /// - public int Priority + public virtual int Priority { get; - private set; + protected set; } internal IntPtr RegistrationPointer @@ -79,8 +89,9 @@ private IntPtr CreateFunctionPointer() return Marshal.GetFunctionPointerForDelegate(EntryPoints.TransportCallback); } - internal void Free() + protected internal void Free() { + Proxy.git_transport_unregister(Prefix, (uint)Priority); Marshal.FreeHGlobal(RegistrationPointer); }