Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maintenance locks and interactivity #598

Conversation

derrickstolee
Copy link
Collaborator

@derrickstolee derrickstolee commented Aug 17, 2023

  • This is an early version of work already under review upstream.
  • This change only applies to interactions with Azure DevOps and the
    GVFS Protocol.
  • This change only applies to the virtualization hook and VFS for Git.

This set of patches has a little of both:

  1. We need to revert a custom change around maintenance.lock files that was only in microsoft/git. The "fix" actually caused a worse situation where many background maintenance processes would pile up.
  2. Create the credential.interactive config as an official Git config and use it in Git to prevent any chance of a foreground username/password request. (This is borrowed from GCM, which already respects this.)
  3. To help avoid problems, especially when blocked by credentials, add configuration to the background schedule to avoid interactive prompts.
  4. To make sure that the new config options are set up in the background schedule, update scalar reconfigure to execute git maintenance start.

Points 2-4 could maybe to upstream, but we shouldn't wait for that.

I've tested these features locally on Linux and Windows and double-checked that they will prevent the backup of maintenance processes when the credentials become invalid.

@derrickstolee derrickstolee self-assigned this Aug 17, 2023
@@ -1678,6 +1657,9 @@ static const char *get_frequency(enum schedule_priority schedule)
}
}

static const char *custom_background_config =
"-c credential.interactive=false -c cred.askPass=false";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit. As an alternative to defining a static string here, you could #define a macro and use that in each of the fprintf() format strings below (like we do for the PRIuMAX or PRItime strings).

credential.c Outdated

if (!git_config_get_int("credential.timeout", &config_seconds) &&
config_seconds >= 0)
select_timeout.tv_sec = config_seconds;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I reading this right? If credential.timeout is not defined in the config, we'll default to 5 minutes? So to avoid the timeout and get the legacy wait, we need to have credential.timeout=0 in the config. Not questioning, just confirming intent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's the intent.

Copy link
Member

@mjcheetham mjcheetham Aug 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note for GCM.. this is a setting we should ignore ourselves :)
As in, GCM itself shouldn't have a timeout, since Git is the one doing the timeout and kill.

credential.c Outdated
return -1;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming that if we get a single byte from GCM, we'll get the full response so we don't need to do something more fancy. GCM will either be stuck or not before the first byte is returned.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's also my expectation. The response is not provided until after the "blocking" part of GCM is complete. @mjcheetham or @ldennington might be able to confirm for sure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GCM only writes back to stdout once it has the complete result. If we've written anything back, we will soon exit.

credential.c Outdated

if (!FD_ISSET(helper.out, &readfds)) {
/* Timeout complete before helper.out has bytes to read. */
kill_child_command(&helper);
Copy link

@jeffhostetler jeffhostetler Aug 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bothers me a bit. I'm wondering if we should just insert a kill() here and then let the existing call to finish_command() at the bottom wait for the child. The existing finish code will end up calling wait_or_whine() which normalizes the child's exit code, so we'll be consistent. And then it'll emit
the child_exit and other cleanup.

You could also add a trace2_data after the kill() to log that we sent the signal, but that is not strictly necessary.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, then you won't need the kill_child_command() function at all. That might make upstream integration easier.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I hadn't considered that, because I expected the behavior to be "kill the child then leave the method" and not to go to a later finish_command(). I will play with this when I have a better testing environment ready.

builtin/gc.c Outdated
@@ -1678,6 +1657,9 @@ static const char *get_frequency(enum schedule_priority schedule)
}
}

static const char *custom_background_config =
"-c credential.interactive=false -c cred.askPass=false";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's worth trying to upstream the concept of credential.interactive, and have it mean "helpers should not interact with the user".

Right now, this is a GCM specific thing.

Alternatively, perhaps a core.interactive or core.background setting would make more sense? To indicate that "this instance of Git is running non-interactively or in the background", and that is a signal that helpers can pick up on to mean "no prompt"?

Copy link

@jeffhostetler jeffhostetler Aug 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be simpler to have core.background as a single value. Then GCM or the ask-pass or whatever code could do the right thing without us having to enumerate the various flags here.

@derrickstolee derrickstolee temporarily deployed to release August 18, 2023 18:58 — with GitHub Actions Inactive
@derrickstolee derrickstolee temporarily deployed to release August 18, 2023 21:01 — with GitHub Actions Inactive
@jeffhostetler
Copy link

Does gc know enough about it's environment to know whether to set the background bit? That is, if gc is run by maintenance, then yes, but if gc is run by a foreground fetch, then no. Right??

@jeffhostetler
Copy link

Same question about credential.c WRT the timeout. If I interactively do a push/fetch and get stuck behind a cred prompt -- and have to dig my phone out of my backpack or while I go get coffee, should it always timeout/abort? Or should the timeout only be enabled in the maintenance case?

Do we still need the timeout if GCM respects the suggested core.background flag ??

@derrickstolee
Copy link
Collaborator Author

Same question about credential.c WRT the timeout. If I interactively do a push/fetch and get stuck behind a cred prompt -- and have to dig my phone out of my backpack or while I go get coffee, should it always timeout/abort? Or should the timeout only be enabled in the maintenance case?

I could consider leaving it as a non-timeout case for foreground and set the config in the maintenance scheduler (like we are already doing for the interactivity bit).

Do we still need the timeout if GCM respects the suggested core.background flag ??

Is that a thing? One thing I think happens is that background jobs don't have a TTY, so we won't get blocked on Git's request for a username/password (which during my local testing requires setting GIT_TERMINAL_PROMPT=0).

@derrickstolee derrickstolee force-pushed the maintenance-locks-and-interactivity branch from f27b3af to a25660f Compare August 21, 2023 16:00
@derrickstolee derrickstolee temporarily deployed to release August 21, 2023 16:00 — with GitHub Actions Inactive
@derrickstolee derrickstolee temporarily deployed to release August 21, 2023 16:04 — with GitHub Actions Inactive
@derrickstolee derrickstolee temporarily deployed to release August 21, 2023 16:10 — with GitHub Actions Inactive
@derrickstolee derrickstolee temporarily deployed to release August 21, 2023 16:25 — with GitHub Actions Inactive
@derrickstolee derrickstolee temporarily deployed to release August 21, 2023 16:32 — with GitHub Actions Inactive
@derrickstolee derrickstolee temporarily deployed to release August 21, 2023 16:32 — with GitHub Actions Inactive
@jeffhostetler
Copy link

Same question about credential.c WRT the timeout. If I interactively do a push/fetch and get stuck behind a cred prompt -- and have to dig my phone out of my backpack or while I go get coffee, should it always timeout/abort? Or should the timeout only be enabled in the maintenance case?

I could consider leaving it as a non-timeout case for foreground and set the config in the maintenance scheduler (like we are already doing for the interactivity bit).

I'll wait for @mjcheetham opinion here, but I'm wondering if we want to change the foreground behavior here.

Do we still need the timeout if GCM respects the suggested core.background flag ??

Is that a thing? One thing I think happens is that background jobs don't have a TTY, so we won't get blocked on Git's request for a username/password (which during my local testing requires setting GIT_TERMINAL_PROMPT=0).

I'm not sure. There are too many child processes with STDIN/OUT bound to a pipe from the parent process for me to casually trust isatty(fd) ...

@derrickstolee
Copy link
Collaborator Author

Do we still need the timeout if GCM respects the suggested core.background flag ??

Is that a thing? One thing I think happens is that background jobs don't have a TTY, so we won't get blocked on Git's request for a username/password (which during my local testing requires setting GIT_TERMINAL_PROMPT=0).

I'm not sure. There are too many child processes with STDIN/OUT bound to a pipe from the parent process for me to casually trust isatty(fd) ...

What's even worse is that the Git prompt for username/password goes through git_terminal_prompt() which pulls the terminal directly from the environment, and ignores something like redirecting /dev/null into stdin.

@derrickstolee derrickstolee temporarily deployed to release August 21, 2023 17:28 — with GitHub Actions Inactive
@derrickstolee
Copy link
Collaborator Author

End-to-End Testing Report

After some local testing in Linux helped identify some issues, I generated a Windows installer and ran it on my Windows machine. Along with an earlier version of the installer, I was able to find out this information:

  • I had made a mistake of using the wrong config key name sometimes when I meant core.askpass. Further, using "false" results in some failures that are not helpful. Instead, using core.askpass=echo makes Git skip the feature and move on completely.
  • In addition to the core.askpass config key, there is a GIT_ASKPASS environment variable that overrides the config key. It's set by VS Code (at least in Remote SSH connections like I use).
  • Even when using an alternative core.askpass, Git has an interactive username/password request that goes through the terminal. See git_terminal_prompt() down deep below credential_getpass() for this info. This doesn't block when using a background job, since there is no terminal, but it makes testing challenging.
  • The previous two bullets might be good reasons to introduce credential.interactive upstream: we can block this behavior when the user is requesting it.
  • While the select() method works appropriately on Linux, on Windows it seems to be returning immediately as "the readfd is ready" instead of waiting for a byte of data to be sent. Because this isn't helping, I will most likely remove the timeout feature from this pull request.
  • However I was able to get things to work by using the -c credential.interactive=false -c core.askpass=echo options. These allow fetches to work when the credentials are valid, and the fetches complete (with expected failure) when the credentials are not valid. This will solve our problems with background fetches getting blocked on credentials.
  • In order to get these custom configs into the background jobs, we actually need the schedule to be updated during upgrade. I added a patch to make scalar reconfigure run git maintenance start in the necessary repos, giving us an updated schedule.

I need to update this branch with the full learnings here, and add some tests now that we understand the code necessary to get the features we need.

@derrickstolee derrickstolee force-pushed the maintenance-locks-and-interactivity branch 4 times, most recently from 1ecfce3 to 8676615 Compare August 22, 2023 16:55
@derrickstolee derrickstolee marked this pull request as ready for review August 22, 2023 17:23
@jeffhostetler
Copy link

nit: typo in commit message of "add new interactive config option": carefult

This change from microsoft#468 is causing multiple maintenance
processes to get blocked on credentials instead of only one. The change
did more harm than good.

This reverts commit 95ed7f6.
@derrickstolee derrickstolee force-pushed the maintenance-locks-and-interactivity branch from 8676615 to 840beed Compare August 22, 2023 19:03
@jeffhostetler
Copy link

i didn't see anything else. thanks!

Copy link
Member

@dscho dscho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am optimistic that this will address the reported problems.

A couple of feedback comments about the commit messages:

  • In the second commit's message: "caues" -> "cause", "modifed" -> "modified".
  • The third commit's message mentions "GCM" without prior explanation of the acronym; I would recommend using "Git Credential Manager" here instead.

char *value;
if (!git_config_get_maybe_bool("credential.interactive", &interactive) &&
!interactive)
return -1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to trace the fact that the interactive credential prompt was skipped?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting idea. I was thinking that the lack of the other region would be enough.

Do you propose using a trace2_printf()? or what kind of indicator? I'm not familiar with an example of this kind of tracing.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could do something like a trace2_data_intmax(... "credential", "interactive/skipped", 1)

only log the true case.

t/t5551-http-fetch-smart.sh Show resolved Hide resolved
builtin/gc.c Outdated Show resolved Hide resolved
builtin/gc.c Outdated Show resolved Hide resolved
t/t9210-scalar.sh Show resolved Hide resolved
@derrickstolee derrickstolee force-pushed the maintenance-locks-and-interactivity branch 2 times, most recently from 73fe2ea to e0b672c Compare August 23, 2023 13:44
When scripts or background maintenance wish to perform HTTP(S) requests,
there is a risk that our stored credentials might be invalid. At the
moment, this causes the credential helper to ping the user and block the
process. Even if the credential helper does not ping the user, Git falls
back to the 'askpass' method, which includes a direct ping to the user
via the terminal.

Even setting the 'core.askPass' config as something like 'echo' will
causes Git to fallback to a terminal prompt. It uses
git_terminal_prompt(), which finds the terminal from the environment and
ignores whether stdin has been redirected. This can also block the
process awaiting input.

Create a new config option to prevent user interaction, favoring a
failure to a blocked process.

The chosen name, 'credential.interactive', is taken from the config
option used by Git Credential Manager to already avoid user
interactivity, so there is already one credential helper that integrates
with this option. However, older versions of Git Credential Manager also
accepted other string values, including 'auto', 'never', and 'always'.
The modern use is to use a boolean value, but we should still be
careful that some users could have these non-booleans. Further, we
should respect 'never' the same as 'false'. This is respected by the
implementation and test, but not mentioned in the documentation.

The implementation for the Git interactions takes place within
credential_getpass(). The method prototype is modified to return an
'int' instead of 'void'. This allows us to detect that no attempt was
made to fill the given credential, changing the single caller slightly.

Also, a new trace2 region is added around the interactive portion of the
credential request. This provides a way to measure the amount of time
spent in that region for commands that _are_ interactive. It also makes
a conventient way to test that the config option works with
'test_region'.

Signed-off-by: Derrick Stolee <derrickstolee@github.com>
At the moment, some background jobs are getting blocked on credentials
during the 'prefetch' task. This leads to other tasks, such as
incremental repacks, getting blocked. Further, if a user manages to fix
their credentials, then they still need to cancel the background process
before their background maintenance can continue working.

Update the background schedules for our four scheduler integrations to
include these config options via '-c' options:

 * 'credential.interactive=false' will stop Git and some credential
   helpers from prompting in the UI (assuming the '-c' parameters are
   carried through and respected by GCM).

 * 'core.askPass=true' will replace the text fallback for a username
   and password into the 'true' command, which will return a success in
   its exit code, but Git will treat the empty string returned as an
   invalid password and move on.

We can do some testing that the credentials are passed, at least in the
systemd case due to writing the service files.

Signed-off-by: Derrick Stolee <derrickstolee@github.com>
The 'scalar reconfigure' command is intended to update registered repos
with the latest settings available. However, up to now we were not
reregistering the repos with background maintenance.

In particular, this meant that the background maintenance schedule would
not be updated if there are improvements between versions.

Be sure to register repos for maintenance during the reconfigure step.

Signed-off-by: Derrick Stolee <derrickstolee@github.com>
In this commit, we added the 'credential.interactive=never' option to
unattended scalar options. This should be changed to 'false' to match
the modern use of this config option.

But also, we have a test that requires using askpass to get credentials,
but the test is in unattended mode. Fix that test to include
'credential.interactive=true' to bypass this issue.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants