AutoVersionUpdate.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. // Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
  2. //
  3. // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
  4. //
  5. // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
  6. #if NET10_0_OR_GREATER
  7. using System.Data;
  8. using System.Diagnostics;
  9. using System.Security.Cryptography;
  10. using System.Text;
  11. using System.Text.RegularExpressions;
  12. using Microsoft.AspNetCore.Builder;
  13. using XiHan.Framework.Utils.Logging;
  14. using XiHan.Framework.Utils.Reflections;
  15. namespace Admin.NET.Core.Update;
  16. /// <summary>
  17. /// 自动版本更新中间件拓展
  18. /// </summary>
  19. [SuppressSniffer]
  20. public static class AutoVersionUpdate
  21. {
  22. private const string MigrationLogTable = "sys_db_migration_log";
  23. private const string StatusRunning = "Running";
  24. private const string StatusSuccess = "Success";
  25. private const string StatusFailed = "Failed";
  26. private const string StatusSkipped = "Skipped";
  27. private const string VerifyNotConfigured = "NotConfigured";
  28. private const string VerifySuccess = "Success";
  29. private const string VerifyFailed = "Failed";
  30. /// <summary>
  31. /// 使用自动版本更新中间件
  32. /// </summary>
  33. public static IApplicationBuilder UseAutoVersionUpdate(this IApplicationBuilder app)
  34. {
  35. LogHelper.Info("AutoVersionUpdate 中间件运行");
  36. var snowIdOpt = App.GetConfig<SnowIdOptions>("SnowId", true);
  37. if (snowIdOpt.WorkerId != 1)
  38. {
  39. LogHelper.Handle("非主节点,不执行脚本");
  40. return app;
  41. }
  42. var stopOnFailure = App.GetConfig<bool?>("AutoVersionUpdate:StopApplicationOnFailure") ?? true;
  43. try
  44. {
  45. RunPendingMigrations(app);
  46. LogHelper.Success("AutoVersionUpdate 中间件结束");
  47. }
  48. catch (Exception ex)
  49. {
  50. LogHelper.Error($"AutoVersionUpdate 执行失败:{ex.Message}");
  51. if (stopOnFailure)
  52. throw;
  53. }
  54. return app;
  55. }
  56. private static void RunPendingMigrations(IApplicationBuilder app)
  57. {
  58. using var scope = App.GetRequiredService<IServiceScopeFactory>().CreateScope();
  59. var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
  60. EnsureMigrationLogTable(db);
  61. var currentVersionText = GetEntryAssemblyCurrentVersion();
  62. var currentVersion = ParseVersion(currentVersionText);
  63. var historyFromTxt = GetEntryAssemblyHistoryVersionInfo();
  64. var databaseName = GetDatabaseName(db);
  65. LogHelper.Handle($"当前版本:{currentVersionText},目标数据库:{databaseName ?? "(unknown)"}");
  66. var scripts = LoadMigrationScripts();
  67. var migrationRows = LoadMigrationLogs(db);
  68. var successVersions = migrationRows
  69. .Where(x => string.Equals(x.Status, StatusSuccess, StringComparison.OrdinalIgnoreCase))
  70. .Select(x => ParseVersion(x.Version))
  71. .ToHashSet();
  72. Version? legacyBoundary = null;
  73. if (migrationRows.Count == 0 &&
  74. !string.IsNullOrWhiteSpace(historyFromTxt.Version) &&
  75. historyFromTxt.IsRunScript &&
  76. Version.TryParse(historyFromTxt.Version, out var legacyVersion))
  77. {
  78. legacyBoundary = legacyVersion;
  79. LogHelper.Handle(
  80. $"检测到 {MigrationLogTable} 为空但 version.txt 存在,legacy 跳过边界:{legacyBoundary}");
  81. }
  82. var pending = scripts
  83. .Where(s => s.ParsedVersion <= currentVersion)
  84. .Where(s => !ShouldSkipScript(s, migrationRows, legacyBoundary))
  85. .OrderBy(s => s.ParsedVersion)
  86. .ToList();
  87. LogHelper.Handle($"发现脚本 {scripts.Count} 个,已成功 {successVersions.Count} 个,待执行 {pending.Count} 个");
  88. if (pending.Count == 0)
  89. {
  90. SetEntryAssemblyCurrentVersion(currentVersionText, true);
  91. return;
  92. }
  93. foreach (var script in pending)
  94. {
  95. ExecuteOneMigrationScript(db, script, databaseName);
  96. }
  97. SetEntryAssemblyCurrentVersion(currentVersionText, true);
  98. }
  99. private static bool ShouldSkipScript(
  100. MigrationScript script,
  101. List<MigrationLogRow> migrationRows,
  102. Version? legacyBoundary)
  103. {
  104. var row = migrationRows.FirstOrDefault(x =>
  105. string.Equals(x.Version, script.Version, StringComparison.OrdinalIgnoreCase));
  106. if (row != null)
  107. {
  108. if (string.Equals(row.Status, StatusSuccess, StringComparison.OrdinalIgnoreCase))
  109. {
  110. if (!string.Equals(row.FileHash, script.Hash, StringComparison.OrdinalIgnoreCase))
  111. {
  112. throw new InvalidOperationException(
  113. $"版本脚本 {script.FileName} 的 SHA256 与已成功记录不一致,禁止静默覆盖。请新建更高版本脚本。");
  114. }
  115. LogHelper.Handle($"版本 {script.Version} 已成功且 hash 未变,跳过");
  116. return true;
  117. }
  118. return false;
  119. }
  120. if (legacyBoundary != null && script.ParsedVersion <= legacyBoundary)
  121. {
  122. LogHelper.Handle($"版本 {script.Version} 处于 legacy 边界内,跳过");
  123. return true;
  124. }
  125. return false;
  126. }
  127. private static void ExecuteOneMigrationScript(ISqlSugarClient db, MigrationScript script, string? databaseName)
  128. {
  129. var sql = File.ReadAllText(script.FilePath);
  130. if (string.IsNullOrWhiteSpace(sql))
  131. {
  132. LogHelper.Handle($"版本 {script.Version} 脚本为空,记录 Skipped");
  133. UpsertMigrationLog(db, script, databaseName, StatusSkipped, 0, 0, VerifyNotConfigured, null, 0);
  134. return;
  135. }
  136. if (SqlScriptSplitter.ContainsDelimiterDirective(sql))
  137. {
  138. throw new InvalidOperationException(
  139. $"版本脚本 {script.FileName} 包含 DELIMITER 指令,当前执行器不支持,请改为普通 SQL 或手工执行。");
  140. }
  141. var startedAt = DateTime.Now;
  142. var sw = Stopwatch.StartNew();
  143. UpsertMigrationLog(db, script, databaseName, StatusRunning, 0, 0, VerifyNotConfigured, null, 0);
  144. try
  145. {
  146. LogHelper.Handle($"执行版本 {script.Version} 脚本 {script.FileName},SHA256={script.Hash}");
  147. var executeResult = SqlScriptSplitter.Execute(db, sql);
  148. var verifyStatus = RunVerifyScriptIfExists(db, script);
  149. sw.Stop();
  150. UpsertMigrationLog(
  151. db,
  152. script,
  153. databaseName,
  154. StatusSuccess,
  155. executeResult.StatementCount,
  156. executeResult.AffectedRows,
  157. verifyStatus,
  158. null,
  159. sw.ElapsedMilliseconds);
  160. LogHelper.Handle(
  161. $"版本 {script.Version} 成功:语句 {executeResult.StatementCount} 条,影响行 {executeResult.AffectedRows},校验 {verifyStatus},耗时 {sw.ElapsedMilliseconds}ms");
  162. }
  163. catch (Exception ex)
  164. {
  165. sw.Stop();
  166. var message = BuildExceptionMessage(ex);
  167. UpsertMigrationLog(
  168. db,
  169. script,
  170. databaseName,
  171. StatusFailed,
  172. 0,
  173. 0,
  174. VerifyFailed,
  175. message,
  176. sw.ElapsedMilliseconds);
  177. LogHelper.Error($"AutoVersionUpdate 版本 {script.Version} 失败:{message}");
  178. throw new InvalidOperationException($"AutoVersionUpdate 版本 {script.Version} 执行失败:{message}", ex);
  179. }
  180. }
  181. private static string RunVerifyScriptIfExists(ISqlSugarClient db, MigrationScript script)
  182. {
  183. var verifyPath = Path.ChangeExtension(script.FilePath, ".verify.sql");
  184. if (!File.Exists(verifyPath))
  185. return VerifyNotConfigured;
  186. var verifySql = File.ReadAllText(verifyPath);
  187. var statements = SqlScriptSplitter.Split(verifySql);
  188. if (statements.Count == 0)
  189. return VerifyNotConfigured;
  190. for (var i = 0; i < statements.Count; i++)
  191. {
  192. var statement = statements[i];
  193. var result = db.Ado.SqlQuerySingle<object>(statement);
  194. if (!IsTruthy(result))
  195. {
  196. throw new InvalidOperationException(
  197. $"校验 SQL 第 {i + 1} 条未通过:{TrimForLog(statement)}");
  198. }
  199. }
  200. return VerifySuccess;
  201. }
  202. private static bool IsTruthy(object? value)
  203. {
  204. if (value == null || value is DBNull)
  205. return false;
  206. return value switch
  207. {
  208. bool b => b,
  209. byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal =>
  210. Convert.ToDecimal(value) != 0,
  211. string s => !string.IsNullOrWhiteSpace(s) &&
  212. !string.Equals(s, "0", StringComparison.OrdinalIgnoreCase) &&
  213. !string.Equals(s, "false", StringComparison.OrdinalIgnoreCase),
  214. _ => true
  215. };
  216. }
  217. private static void EnsureMigrationLogTable(ISqlSugarClient db)
  218. {
  219. db.Ado.ExecuteCommand(
  220. """
  221. CREATE TABLE IF NOT EXISTS sys_db_migration_log (
  222. id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
  223. version VARCHAR(50) NOT NULL COMMENT '脚本版本',
  224. file_name VARCHAR(255) NOT NULL COMMENT '脚本文件名',
  225. file_hash VARCHAR(128) NOT NULL COMMENT '脚本 SHA256',
  226. status VARCHAR(20) NOT NULL COMMENT 'Running/Success/Failed/Skipped',
  227. started_at DATETIME(3) NOT NULL COMMENT '开始时间',
  228. finished_at DATETIME(3) NULL COMMENT '结束时间',
  229. elapsed_ms BIGINT NULL COMMENT '耗时毫秒',
  230. statement_count INT NOT NULL DEFAULT 0 COMMENT '执行语句数',
  231. affected_rows BIGINT NULL COMMENT '影响行数合计',
  232. verify_status VARCHAR(20) NULL COMMENT '校验状态',
  233. error_message LONGTEXT NULL COMMENT '错误信息',
  234. database_name VARCHAR(128) NULL COMMENT '执行数据库',
  235. created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  236. updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  237. PRIMARY KEY (id),
  238. UNIQUE KEY uk_sys_db_migration_log_version (version),
  239. KEY idx_sys_db_migration_log_status (status),
  240. KEY idx_sys_db_migration_log_started_at (started_at)
  241. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库版本脚本执行记录'
  242. """);
  243. }
  244. private static List<MigrationLogRow> LoadMigrationLogs(ISqlSugarClient db)
  245. {
  246. try
  247. {
  248. return db.Ado.SqlQuery<MigrationLogRow>(
  249. $"""
  250. SELECT version AS Version, file_hash AS FileHash, status AS Status
  251. FROM {MigrationLogTable}
  252. """);
  253. }
  254. catch
  255. {
  256. return [];
  257. }
  258. }
  259. private static void UpsertMigrationLog(
  260. ISqlSugarClient db,
  261. MigrationScript script,
  262. string? databaseName,
  263. string status,
  264. int statementCount,
  265. long affectedRows,
  266. string verifyStatus,
  267. string? errorMessage,
  268. long elapsedMs)
  269. {
  270. var now = DateTime.Now;
  271. var existing = db.Ado.SqlQuerySingle<int?>(
  272. $"SELECT id FROM {MigrationLogTable} WHERE version = @version LIMIT 1",
  273. new { version = script.Version });
  274. if (existing is > 0)
  275. {
  276. db.Ado.ExecuteCommand(
  277. $"""
  278. UPDATE {MigrationLogTable}
  279. SET file_name = @fileName,
  280. file_hash = @fileHash,
  281. status = @status,
  282. started_at = CASE WHEN @status = '{StatusRunning}' THEN @startedAt ELSE started_at END,
  283. finished_at = @finishedAt,
  284. elapsed_ms = @elapsedMs,
  285. statement_count = @statementCount,
  286. affected_rows = @affectedRows,
  287. verify_status = @verifyStatus,
  288. error_message = @errorMessage,
  289. database_name = @databaseName,
  290. updated_at = @updatedAt
  291. WHERE version = @version
  292. """,
  293. new
  294. {
  295. version = script.Version,
  296. fileName = script.FileName,
  297. fileHash = script.Hash,
  298. status,
  299. startedAt = now,
  300. finishedAt = status is StatusRunning ? (DateTime?)null : now,
  301. elapsedMs = status is StatusRunning ? (long?)null : elapsedMs,
  302. statementCount,
  303. affectedRows,
  304. verifyStatus,
  305. errorMessage,
  306. databaseName,
  307. updatedAt = now
  308. });
  309. return;
  310. }
  311. db.Ado.ExecuteCommand(
  312. $"""
  313. INSERT INTO {MigrationLogTable}
  314. (version, file_name, file_hash, status, started_at, finished_at, elapsed_ms,
  315. statement_count, affected_rows, verify_status, error_message, database_name)
  316. VALUES
  317. (@version, @fileName, @fileHash, @status, @startedAt, @finishedAt, @elapsedMs,
  318. @statementCount, @affectedRows, @verifyStatus, @errorMessage, @databaseName)
  319. """,
  320. new
  321. {
  322. version = script.Version,
  323. fileName = script.FileName,
  324. fileHash = script.Hash,
  325. status,
  326. startedAt = now,
  327. finishedAt = status is StatusRunning ? (DateTime?)null : now,
  328. elapsedMs = status is StatusRunning ? (long?)null : elapsedMs,
  329. statementCount,
  330. affectedRows,
  331. verifyStatus,
  332. errorMessage,
  333. databaseName
  334. });
  335. }
  336. private static List<MigrationScript> LoadMigrationScripts()
  337. {
  338. var path = Path.Combine(AppContext.BaseDirectory, "UpdateScripts");
  339. if (!Directory.Exists(path))
  340. return [];
  341. return Directory.GetFiles(path, "*.sql", SearchOption.TopDirectoryOnly)
  342. .Where(file =>
  343. {
  344. var name = Path.GetFileName(file);
  345. return !name.EndsWith(".verify.sql", StringComparison.OrdinalIgnoreCase);
  346. })
  347. .Select(file =>
  348. {
  349. var versionText = Path.GetFileNameWithoutExtension(file);
  350. if (!Version.TryParse(versionText, out var parsedVersion))
  351. return null;
  352. return new MigrationScript(
  353. versionText,
  354. parsedVersion,
  355. file,
  356. Path.GetFileName(file),
  357. ComputeSha256(file));
  358. })
  359. .Where(x => x != null)
  360. .Cast<MigrationScript>()
  361. .OrderBy(x => x.ParsedVersion)
  362. .ToList();
  363. }
  364. private static string GetEntryAssemblyCurrentVersion()
  365. {
  366. var entryAssemblyVersion = ReflectionHelper.GetEntryAssemblyVersion();
  367. return entryAssemblyVersion.ToString(3);
  368. }
  369. private static void SetEntryAssemblyCurrentVersion(string version, bool isRunScript)
  370. {
  371. var path = Path.Combine(AppContext.BaseDirectory, "version.txt");
  372. var now = DateTime.Now;
  373. File.WriteAllText(path, $"{version}^{now:yyyy-MM-dd HH:mm:ss}^{isRunScript}");
  374. }
  375. private static HistoryVersionInfo GetEntryAssemblyHistoryVersionInfo()
  376. {
  377. var path = Path.Combine(AppContext.BaseDirectory, "version.txt");
  378. if (!File.Exists(path))
  379. return new HistoryVersionInfo(string.Empty, string.Empty, false);
  380. var info = File.ReadAllText(path);
  381. if (!info.Contains('^'))
  382. return new HistoryVersionInfo(string.Empty, string.Empty, false);
  383. var parts = info.Split('^');
  384. var version = parts.Length > 0 ? parts[0] : string.Empty;
  385. var date = parts.Length > 1 ? parts[1] : string.Empty;
  386. var isRunScript = parts.Length > 2 && parts[2].ToBoolean();
  387. return new HistoryVersionInfo(version, date, isRunScript);
  388. }
  389. private static Version ParseVersion(string value)
  390. {
  391. if (!Version.TryParse(value, out var version))
  392. throw new InvalidOperationException($"非法版本号:{value}");
  393. return version;
  394. }
  395. private static string ComputeSha256(string filePath)
  396. {
  397. using var sha = SHA256.Create();
  398. using var stream = File.OpenRead(filePath);
  399. return Convert.ToHexString(sha.ComputeHash(stream));
  400. }
  401. private static string? GetDatabaseName(ISqlSugarClient db)
  402. {
  403. try
  404. {
  405. return db.Ado.GetString("SELECT DATABASE()");
  406. }
  407. catch
  408. {
  409. return null;
  410. }
  411. }
  412. private static string BuildExceptionMessage(Exception ex)
  413. {
  414. var messages = new List<string>();
  415. for (var current = ex; current != null; current = current.InnerException)
  416. messages.Add(current.Message);
  417. return string.Join(" | ", messages);
  418. }
  419. private static string TrimForLog(string sql, int maxLength = 300)
  420. {
  421. var normalized = sql.Replace('\r', ' ').Replace('\n', ' ').Trim();
  422. return normalized.Length <= maxLength ? normalized : normalized[..maxLength] + "...";
  423. }
  424. private sealed record MigrationScript(
  425. string Version,
  426. Version ParsedVersion,
  427. string FilePath,
  428. string FileName,
  429. string Hash);
  430. private sealed class MigrationLogRow
  431. {
  432. public string Version { get; set; } = string.Empty;
  433. public string FileHash { get; set; } = string.Empty;
  434. public string Status { get; set; } = string.Empty;
  435. }
  436. private sealed record ScriptExecuteResult(int StatementCount, long AffectedRows);
  437. private static class SqlScriptSplitter
  438. {
  439. public static bool ContainsDelimiterDirective(string sql) =>
  440. sql.Contains("DELIMITER", StringComparison.OrdinalIgnoreCase);
  441. public static ScriptExecuteResult Execute(ISqlSugarClient db, string sql)
  442. {
  443. if (RequiresSameSessionExecution(sql))
  444. return ExecuteAsBatch(db, sql);
  445. return ExecuteSplitWithSharedConnection(db, sql);
  446. }
  447. private static bool RequiresSameSessionExecution(string sql) =>
  448. sql.Contains("PREPARE", StringComparison.OrdinalIgnoreCase) ||
  449. Regex.IsMatch(sql, @"\bSET\s+@", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) ||
  450. Regex.IsMatch(sql, @"\bSELECT\b.+\bINTO\s+@", RegexOptions.IgnoreCase | RegexOptions.Singleline);
  451. private static ScriptExecuteResult ExecuteAsBatch(ISqlSugarClient db, string sql)
  452. {
  453. var wasOpen = EnsureConnectionOpen(db);
  454. try
  455. {
  456. var affected = db.Ado.ExecuteCommand(sql);
  457. var statementCount = Math.Max(1, Split(sql).Count);
  458. return new ScriptExecuteResult(statementCount, affected);
  459. }
  460. finally
  461. {
  462. RestoreConnection(db, wasOpen);
  463. }
  464. }
  465. private static ScriptExecuteResult ExecuteSplitWithSharedConnection(ISqlSugarClient db, string sql)
  466. {
  467. var statements = Split(sql);
  468. long affectedRows = 0;
  469. var wasOpen = EnsureConnectionOpen(db);
  470. try
  471. {
  472. for (var i = 0; i < statements.Count; i++)
  473. {
  474. var statement = statements[i];
  475. try
  476. {
  477. affectedRows += db.Ado.ExecuteCommand(statement);
  478. }
  479. catch (Exception ex)
  480. {
  481. throw new InvalidOperationException(
  482. $"第 {i + 1}/{statements.Count} 条语句失败:{TrimForLog(statement)} | {ex.Message}", ex);
  483. }
  484. }
  485. return new ScriptExecuteResult(statements.Count, affectedRows);
  486. }
  487. finally
  488. {
  489. RestoreConnection(db, wasOpen);
  490. }
  491. }
  492. private static bool EnsureConnectionOpen(ISqlSugarClient db)
  493. {
  494. var connection = db.Ado.Connection;
  495. if (connection.State == ConnectionState.Open)
  496. return true;
  497. connection.Open();
  498. return false;
  499. }
  500. private static void RestoreConnection(ISqlSugarClient db, bool wasOpen)
  501. {
  502. if (wasOpen)
  503. return;
  504. var connection = db.Ado.Connection;
  505. if (connection.State == ConnectionState.Open)
  506. connection.Close();
  507. }
  508. public static List<string> Split(string sql)
  509. {
  510. var statements = new List<string>();
  511. if (string.IsNullOrWhiteSpace(sql))
  512. return statements;
  513. var current = new StringBuilder();
  514. var inSingleQuote = false;
  515. var inDoubleQuote = false;
  516. var inBacktick = false;
  517. var inLineComment = false;
  518. var inBlockComment = false;
  519. for (var i = 0; i < sql.Length; i++)
  520. {
  521. var ch = sql[i];
  522. var next = i + 1 < sql.Length ? sql[i + 1] : '\0';
  523. if (inLineComment)
  524. {
  525. current.Append(ch);
  526. if (ch is '\n' or '\r')
  527. inLineComment = false;
  528. continue;
  529. }
  530. if (inBlockComment)
  531. {
  532. current.Append(ch);
  533. if (ch == '*' && next == '/')
  534. {
  535. current.Append(next);
  536. i++;
  537. inBlockComment = false;
  538. }
  539. continue;
  540. }
  541. if (!inSingleQuote && !inDoubleQuote && !inBacktick)
  542. {
  543. if (ch == '-' && next == '-')
  544. {
  545. inLineComment = true;
  546. current.Append(ch);
  547. continue;
  548. }
  549. if (ch == '#')
  550. {
  551. inLineComment = true;
  552. current.Append(ch);
  553. continue;
  554. }
  555. if (ch == '/' && next == '*')
  556. {
  557. inBlockComment = true;
  558. current.Append(ch);
  559. continue;
  560. }
  561. }
  562. if (!inDoubleQuote && !inBacktick && ch == '\'' && !inSingleQuote)
  563. {
  564. inSingleQuote = true;
  565. current.Append(ch);
  566. continue;
  567. }
  568. if (inSingleQuote)
  569. {
  570. current.Append(ch);
  571. if (ch == '\'' && next == '\'')
  572. {
  573. current.Append(next);
  574. i++;
  575. continue;
  576. }
  577. if (ch == '\\' && next != '\0')
  578. {
  579. current.Append(next);
  580. i++;
  581. continue;
  582. }
  583. if (ch == '\'')
  584. inSingleQuote = false;
  585. continue;
  586. }
  587. if (!inSingleQuote && !inBacktick && ch == '"' && !inDoubleQuote)
  588. {
  589. inDoubleQuote = true;
  590. current.Append(ch);
  591. continue;
  592. }
  593. if (inDoubleQuote)
  594. {
  595. current.Append(ch);
  596. if (ch == '"' && next == '"')
  597. {
  598. current.Append(next);
  599. i++;
  600. continue;
  601. }
  602. if (ch == '\\' && next != '\0')
  603. {
  604. current.Append(next);
  605. i++;
  606. continue;
  607. }
  608. if (ch == '"')
  609. inDoubleQuote = false;
  610. continue;
  611. }
  612. if (!inSingleQuote && !inDoubleQuote && ch == '`' && !inBacktick)
  613. {
  614. inBacktick = true;
  615. current.Append(ch);
  616. continue;
  617. }
  618. if (inBacktick)
  619. {
  620. current.Append(ch);
  621. if (ch == '`')
  622. inBacktick = false;
  623. continue;
  624. }
  625. if (ch == ';')
  626. {
  627. AppendStatement(statements, current);
  628. continue;
  629. }
  630. current.Append(ch);
  631. }
  632. AppendStatement(statements, current);
  633. return statements;
  634. }
  635. private static void AppendStatement(List<string> statements, StringBuilder current)
  636. {
  637. var text = current.ToString().Trim();
  638. current.Clear();
  639. if (string.IsNullOrWhiteSpace(text))
  640. return;
  641. statements.Add(text);
  642. }
  643. }
  644. }
  645. public record HistoryVersionInfo(string Version, string Date, bool IsRunScript);
  646. #endif // NET10_0_OR_GREATER