Skip to content

Commit fa2cc18

Browse files
committed
gcm: create symlinks and warn message for gcmcore exec
Create symlinks and shim/copy-executables for the original exec "git-credential-manager-core(.exe)" name for consumers who have not updated to the new name. We detect if the consumer is launching us via the "-core" symlink or executable shim by consulting the platform-native APIs to get the original "argv[0]". All of the .NET APIs sadly don't give us the real "argv[0]", so we need to use native APIs... If we detect use of the old name, print a warning that the user should update their configuration, and a help link for more information.
1 parent a34ee31 commit fa2cc18

File tree

12 files changed

+185
-24
lines changed

12 files changed

+185
-24
lines changed

src/linux/Packaging.Linux/build.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ if [ ! -f "$LINK_TO/git-credential-manager" ]; then
235235
"$LINK_TO/git-credential-manager" || exit 1
236236
fi
237237

238+
# Create legacy symlink with older name
239+
if [ ! -f "$LINK_TO/git-credential-manager-core" ]; then
240+
ln -s -r "$INSTALL_TO/git-credential-manager" \
241+
"$LINK_TO/git-credential-manager-core" || exit 1
242+
fi
243+
238244
if [ $INSTALL_FROM_SOURCE = false ]; then
239245
dpkg-deb --build "$DEBROOT" "$DEBPKG" || exit 1
240246
fi

src/osx/Installer.Mac/scripts/postinstall

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ fi
3030
mkdir -p /usr/local/bin
3131
/bin/ln -Fs "$INSTALL_DESTINATION/git-credential-manager" /usr/local/bin/git-credential-manager
3232

33+
# Create legacy symlink to GCMCore in /usr/local/bin
34+
/bin/ln -Fs "$INSTALL_DESTINATION/git-credential-manager" /usr/local/bin/git-credential-manager-core
35+
3336
# Configure GCM for the current user (running as the current user to avoid root
3437
# from taking ownership of ~/.gitconfig)
3538
sudo -u ${USER} "$INSTALL_DESTINATION/git-credential-manager" configure

src/osx/Installer.Mac/uninstall.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ else
2323
echo "No symlink found."
2424
fi
2525

26+
# Remove legacy symlink
27+
if [ -L /usr/local/bin/git-credential-manager-core ]
28+
then
29+
echo "Deleting legacy symlink..."
30+
rm /usr/local/bin/git-credential-manager-core
31+
else
32+
echo "No legacy symlink found."
33+
fi
34+
2635
# Forget package installation/delete receipt
2736
echo "Removing installation receipt..."
2837
pkgutil --forget com.microsoft.gitcredentialmanager

src/shared/Core/ApplicationBase.cs

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Diagnostics;
33
using System.IO;
4-
using System.Reflection;
54
using System.Text;
65
using System.Threading;
76
using System.Threading.Tasks;
@@ -84,31 +83,18 @@ public Task<int> RunAsync(string[] args)
8483

8584
public static string GetEntryApplicationPath()
8685
{
87-
#if NETFRAMEWORK
88-
// Single file publishing does not exist with .NET Framework so
89-
// we can just use reflection to get the entry assembly path.
90-
return Assembly.GetEntryAssembly().Location;
91-
#else
92-
// Assembly::Location always returns an empty string if the application
93-
// was published as a single file
94-
#pragma warning disable IL3000
95-
bool isSingleFile = string.IsNullOrEmpty(Assembly.GetEntryAssembly()?.Location);
96-
#pragma warning restore IL3000
97-
98-
// Use "argv[0]" to get the full path to the entry executable in
99-
// .NET 5+ when published as a single file.
100-
string[] args = Environment.GetCommandLineArgs();
101-
string candidatePath = args[0];
102-
103-
// If we have not been published as a single file then we must strip the
104-
// ".dll" file extension to get the default AppHost/SuperHost name.
105-
if (!isSingleFile && Path.HasExtension(candidatePath))
86+
string argv0 = PlatformUtils.GetNativeArgv0()
87+
?? Process.GetCurrentProcess().MainModule?.FileName
88+
?? Environment.GetCommandLineArgs()[0];
89+
90+
if (Path.IsPathRooted(argv0))
10691
{
107-
return Path.ChangeExtension(candidatePath, null);
92+
return argv0;
10893
}
10994

110-
return candidatePath;
111-
#endif
95+
return Path.GetFullPath(
96+
Path.Combine(Environment.CurrentDirectory, argv0)
97+
);
11298
}
11399

114100
/// <summary>

src/shared/Core/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ public static class HelpUrls
161161
public const string GcmCredentialStores = "https://aka.ms/gcm/credstores";
162162
public const string GcmWamComSecurity = "https://aka.ms/gcm/wamadmin";
163163
public const string GcmAutoDetect = "https://aka.ms/gcm/autodetect";
164+
public const string GcmExecRename = "https://aka.ms/gcm/rename";
164165
}
165166

166167
private static Version _gcmVersion;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace GitCredentialManager.Interop.MacOS.Native
5+
{
6+
public static class LibC
7+
{
8+
private const string LibCLib = "libc";
9+
10+
[DllImport(LibCLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
11+
public static extern IntPtr _NSGetArgv();
12+
13+
[DllImport(LibCLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
14+
public static extern IntPtr _NSGetProgname();
15+
}
16+
}

src/shared/Core/Interop/Windows/Native/Kernel32.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,30 @@ public static extern bool GetConsoleMode(
226226
public static extern bool SetConsoleMode(
227227
[In] SafeFileHandle consoleHandle,
228228
[In, MarshalAs(UnmanagedType.U4)] ConsoleMode consoleMode);
229+
230+
/// <summary>
231+
/// Retrieves the command-line string for the current process.
232+
/// </summary>
233+
/// <returns>The return value is the command-line string for the current process.</returns>
234+
[DllImport(LibraryName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
235+
public static extern IntPtr GetCommandLine();
236+
237+
/// <summary>
238+
/// Frees the specified local memory object and invalidates its handle.
239+
/// </summary>
240+
/// <param name="ptr">
241+
/// A handle to the local memory object.
242+
/// This handle is returned by either the LocalAlloc or LocalReAlloc function.
243+
/// It is not safe to free memory allocated with GlobalAlloc.
244+
/// </param>
245+
/// <returns>
246+
/// If the function succeeds, the return value is NULL.
247+
/// <para/>
248+
/// If the function fails, the return value is equal to a handle to the local memory object.
249+
/// <para/>
250+
/// To get extended error information, call GetLastError.
251+
/// </returns>
252+
public static extern IntPtr LocalFree(IntPtr ptr);
229253
}
230254

231255
[Flags]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace GitCredentialManager.Interop.Windows.Native
5+
{
6+
public static class Shell32
7+
{
8+
private const string LibraryName = "shell32.dll";
9+
10+
/// <summary>
11+
/// Parses a Unicode command line string and returns an array of pointers
12+
/// to the command line arguments, along with a count of such arguments,
13+
/// in a way that is similar to the standard C run-time argv and argc values.
14+
/// </summary>
15+
/// <param name="lpCmdLine">
16+
/// Pointer to a null-terminated Unicode string that contains the full command line.
17+
/// If this parameter is an empty string the function returns the path to the current executable file.
18+
/// </param>
19+
/// <param name="pNumArgs">
20+
/// Pointer to an int that receives the number of array elements returned, similar to argc.
21+
/// </param>
22+
/// <returns>A pointer to an array of LPWSTR values, similar to argv.</returns>
23+
[DllImport("Shell32.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
24+
public static extern IntPtr CommandLineToArgvW(IntPtr lpCmdLine, out int pNumArgs);
25+
}
26+
}

src/shared/Core/PlatformUtils.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Diagnostics;
3+
using System.IO;
34
using System.Runtime.InteropServices;
45
using GitCredentialManager.Interop.Posix.Native;
56

@@ -175,6 +176,63 @@ public static bool IsElevatedUser()
175176
return false;
176177
}
177178

179+
#region Platform argv[0] Utils
180+
181+
public static string GetNativeArgv0()
182+
{
183+
try
184+
{
185+
if (IsWindows())
186+
{
187+
return GetWindowsArgv0();
188+
}
189+
190+
if (IsMacOS())
191+
{
192+
return GetMacOSArgv0();
193+
}
194+
195+
if (IsLinux())
196+
{
197+
return GetLinuxArgv0();
198+
}
199+
}
200+
catch
201+
{
202+
// If there are any issues getting the native argv[0]
203+
// we should not throw, and certainly not crash!
204+
// Just return null instead.
205+
}
206+
207+
return null;
208+
}
209+
210+
private static string GetLinuxArgv0()
211+
{
212+
string cmdline = File.ReadAllText("/proc/self/cmdline");
213+
return cmdline.Split('\0')[0];
214+
}
215+
216+
private static string GetMacOSArgv0()
217+
{
218+
IntPtr ptr = Interop.MacOS.Native.LibC._NSGetArgv();
219+
IntPtr argvPtr = Marshal.ReadIntPtr(ptr);
220+
IntPtr argv0Ptr = Marshal.ReadIntPtr(argvPtr);
221+
return Marshal.PtrToStringAnsi(argv0Ptr);
222+
}
223+
224+
private static string GetWindowsArgv0()
225+
{
226+
IntPtr argvPtr = Interop.Windows.Native.Shell32.CommandLineToArgvW(
227+
Interop.Windows.Native.Kernel32.GetCommandLine(), out _);
228+
IntPtr argv0Ptr = Marshal.ReadIntPtr(argvPtr);
229+
string argv0 = Marshal.PtrToStringAuto(argv0Ptr);
230+
Interop.Windows.Native.Kernel32.LocalFree(argvPtr);
231+
return argv0;
232+
}
233+
234+
#endregion
235+
178236
#region Platform information helper methods
179237

180238
private static string GetOSType()

src/shared/Git-Credential-Manager/Program.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,29 @@ public static void Main(string[] args)
3131
}
3232
}
3333

34+
//
35+
// Git Credential Manager's executable used to be named "git-credential-manager-core" before
36+
// dropping the "-core" suffix. In order to prevent "helper not found" errors for users who
37+
// haven't updated their configuration, we include either a 'shim' or symlink with the old name
38+
// that print warning messages about using the old name, and then continue execution of GCM.
39+
//
40+
// On Windows the shim is an exact copy of the main "git-credential-manager.exe" executable
41+
// with the old name. We inspect argv[0] to see which executable we are launched as.
42+
//
43+
// On UNIX systems we do the same check, except instead of a copy we use a symlink.
44+
//
45+
string oldName = PlatformUtils.IsWindows()
46+
? "git-credential-manager-core.exe"
47+
: "git-credential-manager-core";
48+
49+
if (appPath?.EndsWith(oldName, StringComparison.OrdinalIgnoreCase) ?? false)
50+
{
51+
context.Streams.Error.WriteLine(
52+
"warning: git-credential-manager-core was renamed to git-credential-manager");
53+
context.Streams.Error.WriteLine(
54+
$"warning: see {Constants.HelpUrls.GcmExecRename} for more information");
55+
}
56+
3457
// Register all supported host providers at the normal priority.
3558
// The generic provider should never win against a more specific one, so register it with low priority.
3659
app.RegisterProvider(new AzureReposHostProvider(context), HostProviderPriority.Normal);

0 commit comments

Comments
 (0)