#498 fix
This commit is contained in:
parent
ae6ff5ba28
commit
fbb2700afb
@ -21211,6 +21211,24 @@
|
||||
current poem count.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:RMuseum.Services.Implementation.GanjoorService._publicDataExportRunningLock">
|
||||
<summary>
|
||||
Guards against two runs of the same export job (keyed by job name — "PublicDataExport"
|
||||
/ "TajikPublicDataExport") ever executing concurrently against the same git working
|
||||
copy. This exists because deleting a job's row on the admin Jobs page only removes its
|
||||
database record — it does not stop the background Task (or its child git.exe process)
|
||||
that's still actually running. Without this guard, triggering the job again while a
|
||||
previous run hadn't finished starts a second concurrent git process against the same
|
||||
folder, which is exactly what produces a ".git/index.lock: File exists" failure.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorService.TryStartExclusiveExportJob(System.String)">
|
||||
<summary>
|
||||
Returns true (and marks <paramref name="jobName"/> as running) if no run of this job
|
||||
was already in progress; false if one was, in which case the caller should refuse to
|
||||
start a second one rather than queueing a background task that would race the first.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorService.StartBatchExportPublicGitData">
|
||||
<summary>
|
||||
start exporting all Ganjoor data (poets/categories/poems/verses) belonging to
|
||||
@ -24160,6 +24178,14 @@
|
||||
any reason (e.g. a Windows service running under a service account with its own PATH).
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Utils.PublicDataExport.GitRepoPublisherOptions.CommandTimeoutMinutes">
|
||||
<summary>
|
||||
how long a single git command is allowed to run before it's killed and the job fails
|
||||
with a clear timeout error, instead of hanging forever (e.g. if git ever ends up
|
||||
waiting on an interactive prompt with no terminal to answer it — see
|
||||
GIT_TERMINAL_PROMPT in GitRepoPublisher)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Utils.PublicDataExport.GitRepoPublisher">
|
||||
<summary>
|
||||
Thin wrapper around the native <c>git</c> CLI (not LibGit2Sharp) for the export job: sync a
|
||||
@ -24187,6 +24213,21 @@
|
||||
be hand-editing it, so a hard reset is safe and keeps the job idempotent).
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Utils.PublicDataExport.GitRepoPublisher.RemoveStaleLockFileIfAny">
|
||||
<summary>
|
||||
A .git/index.lock left behind by an abruptly-terminated git process (e.g. an app pool
|
||||
recycle, or a previous run of this same job that got killed mid-command rather than
|
||||
finishing) blocks every future git command in this working copy with a confusing
|
||||
"Another git process seems to be running" error, even when nothing actually is.
|
||||
GanjoorService's TryStartExclusiveExportJob already guarantees only one run of a given
|
||||
export job (main or Tajik) executes at a time *within this process* — so reaching this
|
||||
method at all means the current call is the sole legitimate owner of this working copy
|
||||
right now, and any lock file found here can only be a leftover from something that is
|
||||
no longer running (that in-process guard doesn't survive an app pool/process restart,
|
||||
which is exactly the scenario that leaves an orphaned lock file in the first place).
|
||||
Safe to remove unconditionally on that basis.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Utils.PublicDataExport.GitRepoPublisher.CommitAndPush(System.String)">
|
||||
<summary>
|
||||
Stages every change under the working copy and, if anything actually changed, commits
|
||||
|
||||
@ -211,6 +211,7 @@ namespace RMuseum.Services.Implementation
|
||||
GitUserName = section["GitUserName"],
|
||||
GitToken = section["GitToken"],
|
||||
GitExecutablePath = section["GitExecutablePath"],
|
||||
CommandTimeoutMinutes = int.TryParse(section["CommandTimeoutMinutes"], out var timeout) ? timeout : 45,
|
||||
};
|
||||
}
|
||||
|
||||
@ -488,6 +489,9 @@ Not available as a static endpoint in this data set yet.
|
||||
return
|
||||
$@"# ganjoor-data
|
||||
|
||||
**[▶ Live demo](https://ganjoor.github.io/mini/)** — مینگنجور, a minimal reading app built
|
||||
entirely on this data, running client-side in your browser with no server of its own.
|
||||
|
||||
Public, git-tracked export of [Ganjoor](https://ganjoor.net)'s poetry content — poets,
|
||||
categories, and poems, allowlisted to contain none of the site's user-account-linked data
|
||||
(comments, bookmarks, edit history, etc.). Generated from
|
||||
|
||||
@ -159,6 +159,7 @@ namespace RMuseum.Services.Implementation
|
||||
GitUserName = section["GitUserName"],
|
||||
GitToken = section["GitToken"],
|
||||
GitExecutablePath = section["GitExecutablePath"],
|
||||
CommandTimeoutMinutes = int.TryParse(section["CommandTimeoutMinutes"], out var timeout) ? timeout : 45,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -54,6 +54,14 @@ namespace RMuseum.Utils.PublicDataExport
|
||||
/// any reason (e.g. a Windows service running under a service account with its own PATH).
|
||||
/// </summary>
|
||||
public string GitExecutablePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// how long a single git command is allowed to run before it's killed and the job fails
|
||||
/// with a clear timeout error, instead of hanging forever (e.g. if git ever ends up
|
||||
/// waiting on an interactive prompt with no terminal to answer it — see
|
||||
/// GIT_TERMINAL_PROMPT in GitRepoPublisher)
|
||||
/// </summary>
|
||||
public int CommandTimeoutMinutes { get; set; } = 45;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -209,12 +217,34 @@ namespace RMuseum.Utils.PublicDataExport
|
||||
WorkingDirectory = workingDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
using (var process = new Process { StartInfo = psi })
|
||||
// Without this, git can end up trying to prompt interactively for credentials or a
|
||||
// host-key confirmation. There is no interactive terminal in this process (it runs
|
||||
// under IIS/a background job) — an unanswered prompt just hangs forever with no error
|
||||
// and no timeout, which is exactly what happened here. This makes git fail fast with
|
||||
// a real error instead.
|
||||
psi.EnvironmentVariables["GIT_TERMINAL_PROMPT"] = "0";
|
||||
|
||||
var stdout = new StringBuilder();
|
||||
var stderr = new StringBuilder();
|
||||
|
||||
using (var process = new Process { StartInfo = psi, EnableRaisingEvents = true })
|
||||
{
|
||||
// Reading both redirected streams asynchronously (rather than e.g.
|
||||
// StandardOutput.ReadToEnd() before WaitForExit()) is required here, not optional:
|
||||
// git writes most of its push/fetch progress to stderr, and synchronously draining
|
||||
// stdout first while nobody drains stderr is a classic .NET Process deadlock if
|
||||
// stderr's OS pipe buffer fills up — the child blocks writing to stderr, the parent
|
||||
// blocks reading stdout, and both wait on each other forever. That deadlock (not a
|
||||
// slow network) is almost certainly what actually produced the multi-day hang this
|
||||
// was fixed after.
|
||||
process.OutputDataReceived += (s, e) => { if (e.Data != null) stdout.AppendLine(e.Data); };
|
||||
process.ErrorDataReceived += (s, e) => { if (e.Data != null) stderr.AppendLine(e.Data); };
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
@ -230,11 +260,37 @@ namespace RMuseum.Utils.PublicDataExport
|
||||
"the full path of git.exe.", exp);
|
||||
}
|
||||
|
||||
string stdout = process.StandardOutput.ReadToEnd();
|
||||
string stderr = process.StandardError.ReadToEnd();
|
||||
process.StandardInput.Close(); // nothing will ever be typed to it
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
bool exited = process.WaitForExit(_options.CommandTimeoutMinutes * 60 * 1000);
|
||||
if (!exited)
|
||||
{
|
||||
TryKill(process);
|
||||
throw new TimeoutException(
|
||||
$"git {RedactAuth(arguments)} did not finish within {_options.CommandTimeoutMinutes} minutes and was killed. " +
|
||||
"This is a hang, not normal slowness for this job — most likely git was waiting on a prompt " +
|
||||
"with no terminal to answer it (should no longer happen with GIT_TERMINAL_PROMPT=0 set above) " +
|
||||
"or a genuine network stall.");
|
||||
}
|
||||
|
||||
// let the async read handlers finish flushing after the process has exited
|
||||
process.WaitForExit();
|
||||
|
||||
return new GitProcessResult { ExitCode = process.ExitCode, StandardOutput = stdout, StandardError = stderr };
|
||||
return new GitProcessResult { ExitCode = process.ExitCode, StandardOutput = stdout.ToString(), StandardError = stderr.ToString() };
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryKill(Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best-effort - if it's already gone, or can't be killed, there's nothing more to do
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -76,7 +76,8 @@
|
||||
"PushEnabled": "False",
|
||||
"GitUserName": "",
|
||||
"GitToken": "",
|
||||
"GitExecutablePath": ""
|
||||
"GitExecutablePath": "",
|
||||
"CommandTimeoutMinutes": "45"
|
||||
},
|
||||
"TajikPublicDataExport": {
|
||||
"LocalWorkingCopyPath": "C:\\ganjoor-tajik-public-data",
|
||||
@ -87,7 +88,8 @@
|
||||
"PushEnabled": "False",
|
||||
"GitUserName": "",
|
||||
"GitToken": "",
|
||||
"GitExecutablePath": ""
|
||||
"GitExecutablePath": "",
|
||||
"CommandTimeoutMinutes": "45"
|
||||
},
|
||||
"ExternalFTPServer": {
|
||||
"Host": "localhost",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user