AutoVersionUpdate.cs 26 KB

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