MesDbBackup.cs 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Drawing;
  5. using System.Drawing.Drawing2D;
  6. using System.IO;
  7. using System.Reflection;
  8. using System.Text;
  9. using System.Text.RegularExpressions;
  10. using System.Threading;
  11. using System.Windows.Forms;
  12. namespace MesDbBackup
  13. {
  14. /// <summary>
  15. /// MES 数据库备份桌面客户端:窗口内完成连接、选目录和备份,不打开浏览器。
  16. /// 实际导出仍调用 backup-db.ps1,不改原备份逻辑。
  17. /// </summary>
  18. internal static class Program
  19. {
  20. internal const string MutexName = "Global\\MES_Cloud_DB_Backup_UI";
  21. internal const string MarkerName = ".packed-by-mes-backup";
  22. [STAThread]
  23. static void Main()
  24. {
  25. Application.EnableVisualStyles();
  26. Application.SetCompatibleTextRenderingDefault(false);
  27. bool createdNew;
  28. using (var mutex = new Mutex(true, MutexName, out createdNew))
  29. {
  30. if (!createdNew)
  31. {
  32. MessageBox.Show(
  33. "备份客户端已经在运行,请查看任务栏。",
  34. "MES 数据库备份",
  35. MessageBoxButtons.OK,
  36. MessageBoxIcon.Information);
  37. return;
  38. }
  39. Application.Run(new MainForm());
  40. }
  41. }
  42. }
  43. /// <summary>
  44. /// 深空 AI 主题:暗底玻璃卡片 + 青绿/紫罗兰高光。
  45. /// </summary>
  46. internal static class Theme
  47. {
  48. public static readonly Color Bg = Color.FromArgb(7, 8, 13);
  49. public static readonly Color Header = Color.FromArgb(9, 12, 20);
  50. public static readonly Color HeaderText = Color.FromArgb(238, 244, 255);
  51. public static readonly Color HeaderMuted = Color.FromArgb(139, 149, 171);
  52. public static readonly Color Cyan = Color.FromArgb(94, 234, 212);
  53. public static readonly Color CyanDeep = Color.FromArgb(34, 211, 238);
  54. public static readonly Color Violet = Color.FromArgb(129, 140, 248);
  55. public static readonly Color Ink = Color.FromArgb(6, 16, 24);
  56. public static readonly Color Gold = Color.FromArgb(94, 234, 212);
  57. public static readonly Color Card = Color.FromArgb(18, 22, 32);
  58. public static readonly Color Line = Color.FromArgb(48, 80, 92, 108);
  59. public static readonly Color GhostBorder = Color.FromArgb(70, 255, 255, 255);
  60. public static readonly Color Text = Color.FromArgb(238, 244, 255);
  61. public static readonly Color Muted = Color.FromArgb(139, 149, 171);
  62. public static readonly Color Accent = Color.FromArgb(34, 211, 238);
  63. public static readonly Color AccentHover = Color.FromArgb(94, 234, 212);
  64. public static readonly Color Ok = Color.FromArgb(52, 211, 153);
  65. public static readonly Color Warn = Color.FromArgb(251, 191, 36);
  66. public static readonly Color Bad = Color.FromArgb(251, 113, 133);
  67. public static readonly Color Input = Color.FromArgb(8, 10, 16);
  68. public static readonly Color InputMuted = Color.FromArgb(12, 16, 24);
  69. public static readonly Color Go = Color.FromArgb(34, 211, 238);
  70. public static readonly Color LogBg = Color.FromArgb(5, 7, 12);
  71. public static readonly Color LogText = Color.FromArgb(159, 232, 220);
  72. public static readonly Color Shadow = Color.FromArgb(90, 0, 0, 0);
  73. public static readonly Color Mesh = Color.FromArgb(18, 148, 163, 184);
  74. public static readonly Font Ui = new Font("Microsoft YaHei UI", 10F);
  75. public static readonly Font Title = new Font("Microsoft YaHei UI", 16F, FontStyle.Bold);
  76. public static readonly Font Caption = new Font("Microsoft YaHei UI", 8.5F);
  77. public static readonly Font Section = new Font("Microsoft YaHei UI", 11F, FontStyle.Bold);
  78. public static readonly Font Mono = new Font("Consolas", 9F);
  79. public static readonly Font Mark = new Font("Segoe UI", 16F, FontStyle.Bold);
  80. public static GraphicsPath RoundRect(Rectangle bounds, int radius)
  81. {
  82. var path = new GraphicsPath();
  83. int d = Math.Max(2, radius * 2);
  84. path.AddArc(bounds.X, bounds.Y, d, d, 180, 90);
  85. path.AddArc(bounds.Right - d, bounds.Y, d, d, 270, 90);
  86. path.AddArc(bounds.Right - d, bounds.Bottom - d, d, d, 0, 90);
  87. path.AddArc(bounds.X, bounds.Bottom - d, d, d, 90, 90);
  88. path.CloseFigure();
  89. return path;
  90. }
  91. }
  92. /// <summary>
  93. /// 暗色玻璃卡片,顶部一道青绿高光。
  94. /// </summary>
  95. internal sealed class CardPanel : Panel
  96. {
  97. public CardPanel()
  98. {
  99. SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
  100. BackColor = Color.Transparent;
  101. Padding = new Padding(8);
  102. Margin = new Padding(0, 0, 0, 12);
  103. }
  104. protected override void OnPaint(PaintEventArgs e)
  105. {
  106. e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
  107. var rect = new Rectangle(8, 4, Math.Max(8, Width - 18), Math.Max(8, Height - 12));
  108. using (GraphicsPath shadow = Theme.RoundRect(new Rectangle(rect.X + 3, rect.Y + 6, rect.Width, rect.Height), 16))
  109. using (var brush = new SolidBrush(Theme.Shadow))
  110. {
  111. e.Graphics.FillPath(brush, shadow);
  112. }
  113. using (GraphicsPath path = Theme.RoundRect(rect, 16))
  114. using (var fill = new SolidBrush(Theme.Card))
  115. using (var pen = new Pen(Color.FromArgb(55, 94, 234, 212)))
  116. {
  117. e.Graphics.FillPath(fill, path);
  118. e.Graphics.DrawPath(pen, path);
  119. }
  120. using (var glow = new Pen(Color.FromArgb(140, 94, 234, 212), 1F))
  121. {
  122. e.Graphics.DrawLine(glow, rect.X + 18, rect.Y + 1, rect.Right - 18, rect.Y + 1);
  123. }
  124. }
  125. }
  126. /// <summary>
  127. /// 青绿渐变进度条,替代系统默认绿条。
  128. /// </summary>
  129. internal sealed class AiProgressBar : Control
  130. {
  131. int _value;
  132. int _phase;
  133. ProgressBarStyle _style = ProgressBarStyle.Continuous;
  134. readonly System.Windows.Forms.Timer _tick;
  135. public int Minimum;
  136. public int Maximum = 100;
  137. public AiProgressBar()
  138. {
  139. SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
  140. Height = 8;
  141. _tick = new System.Windows.Forms.Timer();
  142. _tick.Interval = 28;
  143. _tick.Tick += delegate
  144. {
  145. _phase = (_phase + 5) % 400;
  146. Invalidate();
  147. };
  148. }
  149. public int Value
  150. {
  151. get { return _value; }
  152. set
  153. {
  154. _value = Math.Max(Minimum, Math.Min(Maximum, value));
  155. Invalidate();
  156. }
  157. }
  158. public ProgressBarStyle Style
  159. {
  160. get { return _style; }
  161. set
  162. {
  163. _style = value;
  164. if (_style == ProgressBarStyle.Marquee)
  165. {
  166. _tick.Start();
  167. }
  168. else
  169. {
  170. _tick.Stop();
  171. }
  172. Invalidate();
  173. }
  174. }
  175. protected override void OnPaint(PaintEventArgs e)
  176. {
  177. if (Width < 4 || Height < 4)
  178. {
  179. return;
  180. }
  181. e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
  182. var bg = new Rectangle(0, 0, Math.Max(1, Width - 1), Math.Max(1, Height - 1));
  183. using (GraphicsPath path = Theme.RoundRect(bg, Math.Max(2, Height / 2)))
  184. using (var fill = new SolidBrush(Color.FromArgb(255, 12, 16, 24)))
  185. {
  186. e.Graphics.FillPath(fill, path);
  187. }
  188. if (_style == ProgressBarStyle.Marquee)
  189. {
  190. int w = Math.Max(48, Width / 3);
  191. int x = (_phase * (Width + w) / 400) - w;
  192. using (var lg = new LinearGradientBrush(new Rectangle(x, 0, w, Height), Theme.CyanDeep, Theme.Violet, 0f))
  193. {
  194. e.Graphics.FillRectangle(lg, new Rectangle(x, 1, w, Math.Max(1, Height - 2)));
  195. }
  196. }
  197. else if (_value > 0 && Maximum > 0)
  198. {
  199. int w = Math.Max(4, (int)((Width - 2) * (_value / (double)Maximum)));
  200. using (var lg = new LinearGradientBrush(new Rectangle(0, 0, w, Height), Theme.CyanDeep, Theme.Violet, 0f))
  201. using (GraphicsPath path = Theme.RoundRect(new Rectangle(0, 0, w, Math.Max(1, Height - 1)), Math.Max(2, Height / 2)))
  202. {
  203. e.Graphics.FillPath(lg, path);
  204. }
  205. }
  206. }
  207. protected override void Dispose(bool disposing)
  208. {
  209. if (disposing)
  210. {
  211. _tick.Dispose();
  212. }
  213. base.Dispose(disposing);
  214. }
  215. }
  216. internal sealed class DbConfig
  217. {
  218. public string Engine = "mysql";
  219. public string Host = "127.0.0.1";
  220. public int Port = 3306;
  221. public string Database = "mes_cloud";
  222. public string User = "mes_cloud";
  223. public string Password = "s2ENj7KaEySTC7dW";
  224. }
  225. internal static class DbConfigStore
  226. {
  227. public static string ConfigPath(string workDir)
  228. {
  229. return Path.Combine(workDir, "db-config.json");
  230. }
  231. public static DbConfig Load(string workDir)
  232. {
  233. string path = ConfigPath(workDir);
  234. if (!File.Exists(path))
  235. {
  236. return new DbConfig();
  237. }
  238. try
  239. {
  240. string raw = File.ReadAllText(path, Encoding.UTF8);
  241. var lines = new List<string>();
  242. foreach (string line in raw.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None))
  243. {
  244. if (!line.TrimStart().StartsWith("#"))
  245. {
  246. lines.Add(line);
  247. }
  248. }
  249. string json = string.Join("\n", lines.ToArray()).Trim();
  250. int start = json.IndexOf('{');
  251. int end = json.LastIndexOf('}');
  252. if (start >= 0 && end > start)
  253. {
  254. json = json.Substring(start, end - start + 1);
  255. }
  256. var cfg = new DbConfig();
  257. cfg.Host = ReadString(json, "host", cfg.Host);
  258. cfg.Database = ReadString(json, "database", cfg.Database);
  259. cfg.User = ReadString(json, "user", cfg.User);
  260. cfg.Password = ReadString(json, "password", cfg.Password);
  261. int port = ReadInt(json, "port", cfg.Port);
  262. if (port < 1 || port > 65535)
  263. {
  264. throw new Exception("端口必须在 1-65535");
  265. }
  266. cfg.Port = port;
  267. cfg.Host = NormalizeHost(cfg.Host);
  268. if (string.IsNullOrWhiteSpace(cfg.User))
  269. {
  270. throw new Exception("请填写账号");
  271. }
  272. return cfg;
  273. }
  274. catch
  275. {
  276. return new DbConfig();
  277. }
  278. }
  279. public static void Save(string workDir, DbConfig cfg)
  280. {
  281. cfg.Host = NormalizeHost(cfg.Host);
  282. if (string.IsNullOrWhiteSpace(cfg.User))
  283. {
  284. throw new Exception("请填写账号");
  285. }
  286. if (cfg.Port < 1 || cfg.Port > 65535)
  287. {
  288. throw new Exception("端口必须在 1-65535");
  289. }
  290. string json =
  291. "{\r\n" +
  292. " \"engine\": \"mysql\",\r\n" +
  293. " \"host\": \"" + Escape(cfg.Host) + "\",\r\n" +
  294. " \"port\": " + cfg.Port + ",\r\n" +
  295. " \"database\": \"" + Escape(cfg.Database) + "\",\r\n" +
  296. " \"user\": \"" + Escape(cfg.User) + "\",\r\n" +
  297. " \"password\": \"" + Escape(cfg.Password) + "\"\r\n" +
  298. "}";
  299. File.WriteAllText(ConfigPath(workDir), json, new UTF8Encoding(false));
  300. }
  301. static string NormalizeHost(string host)
  302. {
  303. if (string.IsNullOrWhiteSpace(host))
  304. {
  305. return "127.0.0.1";
  306. }
  307. host = host.Trim();
  308. Match jdbc = Regex.Match(host, @"jdbc:mysql://([^:/]+)");
  309. if (jdbc.Success)
  310. {
  311. host = jdbc.Groups[1].Value;
  312. }
  313. if (host.StartsWith("mysql://", StringComparison.OrdinalIgnoreCase))
  314. {
  315. host = host.Substring(8);
  316. }
  317. if (Regex.IsMatch(host, @"\s|/"))
  318. {
  319. throw new Exception("IP/主机名不合法");
  320. }
  321. return host;
  322. }
  323. static string ReadString(string json, string key, string fallback)
  324. {
  325. Match m = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\"");
  326. if (!m.Success)
  327. {
  328. return fallback;
  329. }
  330. return Unescape(m.Groups[1].Value);
  331. }
  332. static int ReadInt(string json, string key, int fallback)
  333. {
  334. Match m = Regex.Match(json, "\"" + key + "\"\\s*:\\s*(-?\\d+)");
  335. int n;
  336. if (m.Success && int.TryParse(m.Groups[1].Value, out n))
  337. {
  338. return n;
  339. }
  340. return fallback;
  341. }
  342. static string Escape(string s)
  343. {
  344. if (s == null)
  345. {
  346. return "";
  347. }
  348. return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r").Replace("\n", "\\n");
  349. }
  350. static string Unescape(string s)
  351. {
  352. var sb = new StringBuilder();
  353. for (int i = 0; i < s.Length; i++)
  354. {
  355. if (s[i] == '\\' && i + 1 < s.Length)
  356. {
  357. char n = s[++i];
  358. if (n == '"') { sb.Append('"'); }
  359. else if (n == '\\') { sb.Append('\\'); }
  360. else if (n == 'n') { sb.Append('\n'); }
  361. else if (n == 'r') { sb.Append('\r'); }
  362. else if (n == 't') { sb.Append('\t'); }
  363. else { sb.Append(n); }
  364. }
  365. else
  366. {
  367. sb.Append(s[i]);
  368. }
  369. }
  370. return sb.ToString();
  371. }
  372. }
  373. internal static class MysqlUtil
  374. {
  375. static readonly Dictionary<string, string> Cache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  376. public static string FindTool(string name)
  377. {
  378. if (Cache.ContainsKey(name))
  379. {
  380. return Cache[name];
  381. }
  382. string exeName = name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? name : name + ".exe";
  383. string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? "";
  384. foreach (string dir in pathEnv.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
  385. {
  386. try
  387. {
  388. string p = Path.Combine(dir.Trim().Trim('"'), exeName);
  389. if (File.Exists(p))
  390. {
  391. Cache[name] = p;
  392. return p;
  393. }
  394. }
  395. catch { }
  396. }
  397. string[] roots = new[]
  398. {
  399. @"C:\Program Files\MySQL",
  400. @"C:\Program Files (x86)\MySQL",
  401. @"C:\mysql",
  402. @"D:\MySQL",
  403. @"E:\MySQL",
  404. @"C:\Program Files\MariaDB"
  405. };
  406. foreach (string root in roots)
  407. {
  408. if (!Directory.Exists(root))
  409. {
  410. continue;
  411. }
  412. try
  413. {
  414. foreach (string file in Directory.GetFiles(root, exeName, SearchOption.AllDirectories))
  415. {
  416. string bin = Path.GetFileName(Path.GetDirectoryName(file));
  417. if (string.Equals(bin, "bin", StringComparison.OrdinalIgnoreCase))
  418. {
  419. Cache[name] = file;
  420. return file;
  421. }
  422. }
  423. }
  424. catch { }
  425. }
  426. return null;
  427. }
  428. public static void WriteCnf(DbConfig cfg, string cnfPath)
  429. {
  430. string pwd = cfg.Password ?? "";
  431. pwd = pwd.Replace("\\", "\\\\").Replace("\"", "\\\"");
  432. string text =
  433. "[client]\r\n" +
  434. "host=" + cfg.Host + "\r\n" +
  435. "port=" + cfg.Port + "\r\n" +
  436. "user=" + cfg.User + "\r\n" +
  437. "password=\"" + pwd + "\"\r\n" +
  438. "default-character-set=utf8mb4\r\n";
  439. File.WriteAllText(cnfPath, text, Encoding.ASCII);
  440. }
  441. public static string TestConnection(DbConfig cfg)
  442. {
  443. string mysql = FindTool("mysql");
  444. if (mysql == null)
  445. {
  446. throw new Exception("找不到 mysql.exe,请安装 MySQL 客户端");
  447. }
  448. string cnf = Path.Combine(Path.GetTempPath(), "mes_ui_test_" + Process.GetCurrentProcess().Id + ".cnf");
  449. WriteCnf(cfg, cnf);
  450. try
  451. {
  452. string output;
  453. int code = Run(mysql, new[]
  454. {
  455. "--defaults-extra-file=" + cnf,
  456. "--connect-timeout=5",
  457. "-N", "-s", "-e", "SELECT 1"
  458. }, 15000, out output);
  459. if (code != 0)
  460. {
  461. throw new Exception("连接失败: " + (output ?? "").Trim());
  462. }
  463. }
  464. finally
  465. {
  466. TryDelete(cnf);
  467. }
  468. return "MySQL 连接成功: " + cfg.Host + ":" + cfg.Port + " / " + cfg.Database;
  469. }
  470. public static long GetDbSizeBytes(DbConfig cfg)
  471. {
  472. try
  473. {
  474. string mysql = FindTool("mysql");
  475. if (mysql == null)
  476. {
  477. return 0;
  478. }
  479. string cnf = Path.Combine(Path.GetTempPath(), "mes_ui_size_" + Process.GetCurrentProcess().Id + ".cnf");
  480. WriteCnf(cfg, cnf);
  481. try
  482. {
  483. string db = (cfg.Database ?? "").Replace("'", "''");
  484. string sql = "SELECT IFNULL(SUM(data_length+index_length),0) FROM information_schema.tables WHERE table_schema='" + db + "'";
  485. string output;
  486. int code = Run(mysql, new[]
  487. {
  488. "--defaults-extra-file=" + cnf,
  489. "--connect-timeout=3",
  490. "-N", "-s", "-e", sql
  491. }, 8000, out output);
  492. if (code != 0)
  493. {
  494. return 0;
  495. }
  496. long n;
  497. if (long.TryParse((output ?? "").Trim(), out n))
  498. {
  499. return n;
  500. }
  501. }
  502. finally
  503. {
  504. TryDelete(cnf);
  505. }
  506. }
  507. catch { }
  508. return 0;
  509. }
  510. static int Run(string file, string[] args, int timeoutMs, out string output)
  511. {
  512. var psi = new ProcessStartInfo();
  513. psi.FileName = file;
  514. psi.Arguments = QuoteArgs(args);
  515. psi.UseShellExecute = false;
  516. psi.CreateNoWindow = true;
  517. psi.RedirectStandardOutput = true;
  518. psi.RedirectStandardError = true;
  519. psi.StandardOutputEncoding = Encoding.UTF8;
  520. psi.StandardErrorEncoding = Encoding.UTF8;
  521. using (var proc = new Process())
  522. {
  523. proc.StartInfo = psi;
  524. var sb = new StringBuilder();
  525. proc.OutputDataReceived += delegate(object s, DataReceivedEventArgs e)
  526. {
  527. if (!string.IsNullOrEmpty(e.Data)) { sb.AppendLine(e.Data); }
  528. };
  529. proc.ErrorDataReceived += delegate(object s, DataReceivedEventArgs e)
  530. {
  531. if (!string.IsNullOrEmpty(e.Data)) { sb.AppendLine(e.Data); }
  532. };
  533. proc.Start();
  534. proc.BeginOutputReadLine();
  535. proc.BeginErrorReadLine();
  536. if (!proc.WaitForExit(timeoutMs))
  537. {
  538. try { proc.Kill(); } catch { }
  539. throw new Exception("等待超时");
  540. }
  541. proc.WaitForExit();
  542. output = sb.ToString();
  543. return proc.ExitCode;
  544. }
  545. }
  546. static string QuoteArgs(string[] args)
  547. {
  548. var sb = new StringBuilder();
  549. for (int i = 0; i < args.Length; i++)
  550. {
  551. if (i > 0) { sb.Append(' '); }
  552. string a = args[i] ?? "";
  553. sb.Append('"').Append(a.Replace("\"", "\\\"")).Append('"');
  554. }
  555. return sb.ToString();
  556. }
  557. static void TryDelete(string path)
  558. {
  559. try { if (File.Exists(path)) { File.Delete(path); } } catch { }
  560. }
  561. }
  562. internal sealed class MainForm : Form
  563. {
  564. static readonly string[] PackedFiles = new string[]
  565. {
  566. "backup-ui-server.ps1",
  567. "backup-db.ps1",
  568. "db-config.ps1",
  569. "backup-ui.html",
  570. "restore-db.ps1",
  571. "install-backup-task.bat",
  572. "使用说明.txt"
  573. };
  574. readonly string _workDir;
  575. readonly TextBox _host;
  576. readonly TextBox _port;
  577. readonly TextBox _database;
  578. readonly TextBox _user;
  579. readonly TextBox _password;
  580. readonly Label _cfgStatus;
  581. readonly Label _picked;
  582. readonly Label _spaceHint;
  583. readonly Label _progHint;
  584. readonly Label _runStatus;
  585. readonly CheckBox _keepOld;
  586. readonly AiProgressBar _bar;
  587. readonly TextBox _log;
  588. readonly Button _btnTest;
  589. readonly Button _btnSave;
  590. readonly Button _btnBrowse;
  591. readonly Button _btnBackup;
  592. readonly System.Windows.Forms.Timer _poll;
  593. string _selectedPath = "";
  594. Process _backupProc;
  595. string _outFile = "";
  596. DateTime _startedAt;
  597. long _dbBytes;
  598. bool _running;
  599. public MainForm()
  600. {
  601. string exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
  602. if (string.IsNullOrEmpty(exeDir))
  603. {
  604. exeDir = AppDomain.CurrentDomain.BaseDirectory;
  605. }
  606. _workDir = PrepareWorkDir(exeDir);
  607. Text = "MES 智能备份控制台";
  608. Font = Theme.Ui;
  609. BackColor = Theme.Bg;
  610. ForeColor = Theme.Text;
  611. StartPosition = FormStartPosition.CenterScreen;
  612. MinimumSize = new Size(1020, 680);
  613. Size = new Size(1140, 740);
  614. DoubleBuffered = true;
  615. var header = new Panel
  616. {
  617. Dock = DockStyle.Top,
  618. Height = 96,
  619. BackColor = Theme.Header
  620. };
  621. header.Paint += delegate(object s, PaintEventArgs e)
  622. {
  623. e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
  624. e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
  625. using (var g = new LinearGradientBrush(header.ClientRectangle,
  626. Color.FromArgb(10, 14, 24), Color.FromArgb(18, 12, 36), LinearGradientMode.Horizontal))
  627. {
  628. e.Graphics.FillRectangle(g, header.ClientRectangle);
  629. }
  630. using (var glow = new Pen(Color.FromArgb(180, 94, 234, 212), 2F))
  631. {
  632. e.Graphics.DrawLine(glow, 0, 0, header.Width, 0);
  633. }
  634. using (var line = new Pen(Color.FromArgb(60, 129, 140, 248)))
  635. {
  636. e.Graphics.DrawLine(line, 0, header.Height - 1, header.Width, header.Height - 1);
  637. }
  638. var markRect = new Rectangle(26, 24, 48, 48);
  639. using (GraphicsPath path = Theme.RoundRect(markRect, 14))
  640. using (var g = new LinearGradientBrush(markRect, Theme.Cyan, Theme.Violet, 145f))
  641. {
  642. e.Graphics.FillPath(g, path);
  643. }
  644. TextRenderer.DrawText(e.Graphics, "M", Theme.Mark, markRect, Theme.Ink,
  645. TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
  646. TextRenderer.DrawText(e.Graphics, "NEURAL ARCHIVE", Theme.Caption, new Point(86, 16), Theme.Cyan);
  647. TextRenderer.DrawText(e.Graphics, "MES 智能备份控制台", Theme.Title, new Point(84, 34), Theme.HeaderText);
  648. TextRenderer.DrawText(e.Graphics, "独立工具 · 仅 MySQL · 不接入生产系统", Theme.Caption, new Point(86, 66), Theme.HeaderMuted);
  649. string[] chips = new[] { "AIR-GAPPED", "MYSQL", "LOCAL ONLY" };
  650. int x = header.Width - 22;
  651. for (int i = 0; i < chips.Length; i++)
  652. {
  653. Size sz = TextRenderer.MeasureText(chips[i], Theme.Caption);
  654. int w = sz.Width + 16;
  655. x -= w + 8;
  656. var chipRect = new Rectangle(x, 36, w, 24);
  657. using (GraphicsPath path = Theme.RoundRect(chipRect, 12))
  658. using (var fill = new SolidBrush(Color.FromArgb(40, 255, 255, 255)))
  659. using (var pen = new Pen(i == 2 ? Color.FromArgb(120, 94, 234, 212) : Color.FromArgb(50, 255, 255, 255)))
  660. {
  661. e.Graphics.FillPath(fill, path);
  662. e.Graphics.DrawPath(pen, path);
  663. }
  664. TextRenderer.DrawText(e.Graphics, chips[i], Theme.Caption, chipRect,
  665. i == 2 ? Theme.Cyan : Theme.Muted,
  666. TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
  667. }
  668. };
  669. var grid = new TableLayoutPanel();
  670. grid.Dock = DockStyle.Fill;
  671. grid.ColumnCount = 2;
  672. grid.RowCount = 1;
  673. grid.BackColor = Theme.Bg;
  674. grid.Padding = new Padding(18, 12, 14, 12);
  675. grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 56F));
  676. grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 44F));
  677. grid.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
  678. grid.Paint += PaintMesh;
  679. _host = MakeInput();
  680. _port = MakeInput();
  681. _database = MakeInput();
  682. _user = MakeInput();
  683. _password = MakeInput();
  684. _password.UseSystemPasswordChar = true;
  685. var engine = MakeInput();
  686. engine.Text = "MySQL";
  687. engine.ReadOnly = true;
  688. engine.BackColor = Theme.InputMuted;
  689. engine.ForeColor = Theme.Muted;
  690. engine.TabStop = false;
  691. var connCard = MakeCard();
  692. var connBody = MakeBody();
  693. connBody.RowCount = 4;
  694. connBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
  695. connBody.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
  696. connBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 42));
  697. connBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 24));
  698. var connHead = MakeHead("01", "连接矩阵", "先测试,再保存", null);
  699. var form = new TableLayoutPanel();
  700. form.Dock = DockStyle.Fill;
  701. form.ColumnCount = 2;
  702. form.RowCount = 3;
  703. form.BackColor = Theme.Card;
  704. form.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
  705. form.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
  706. for (int i = 0; i < 3; i++)
  707. {
  708. form.RowStyles.Add(new RowStyle(SizeType.Percent, 33.3F));
  709. }
  710. form.Controls.Add(MakeField("类型", engine), 0, 0);
  711. form.Controls.Add(MakeField("端口", _port), 1, 0);
  712. form.Controls.Add(MakeField("IP / 主机", _host), 0, 1);
  713. form.Controls.Add(MakeField("库名", _database), 1, 1);
  714. form.Controls.Add(MakeField("账号", _user), 0, 2);
  715. form.Controls.Add(MakeField("密码", _password), 1, 2);
  716. var connActions = new Panel { Dock = DockStyle.Fill, BackColor = Theme.Card };
  717. _btnTest = MakeButton("测试连接", false);
  718. _btnTest.Location = new Point(4, 4);
  719. _btnSave = MakeGoldButton("保存配置");
  720. _btnSave.Location = new Point(122, 4);
  721. _btnTest.Click += OnTest;
  722. _btnSave.Click += OnSave;
  723. connActions.Controls.Add(_btnTest);
  724. connActions.Controls.Add(_btnSave);
  725. _cfgStatus = new Label
  726. {
  727. Dock = DockStyle.Fill,
  728. TextAlign = ContentAlignment.MiddleLeft,
  729. ForeColor = Theme.Muted,
  730. BackColor = Theme.Card,
  731. Padding = new Padding(6, 0, 0, 0),
  732. Text = "填写 IP、账号、密码后点「测试连接」"
  733. };
  734. connBody.Controls.Add(connHead, 0, 0);
  735. connBody.Controls.Add(form, 0, 1);
  736. connBody.Controls.Add(connActions, 0, 2);
  737. connBody.Controls.Add(_cfgStatus, 0, 3);
  738. connCard.Controls.Add(connBody);
  739. var bakCard = MakeCard();
  740. var bakBody = MakeBody();
  741. bakBody.RowCount = 7;
  742. bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
  743. bakBody.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
  744. bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 22));
  745. bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 28));
  746. bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 12));
  747. bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 22));
  748. bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 48));
  749. _btnBrowse = MakeButton("浏览文件夹", false);
  750. _btnBrowse.Width = 128;
  751. _btnBrowse.Click += OnBrowse;
  752. var bakHead = MakeHead("02", "归档目标", null, _btnBrowse);
  753. _picked = new Label
  754. {
  755. Dock = DockStyle.Fill,
  756. BackColor = Theme.Input,
  757. ForeColor = Theme.Muted,
  758. Text = " 尚未选择保存位置",
  759. TextAlign = ContentAlignment.MiddleLeft,
  760. Padding = new Padding(12, 0, 12, 0)
  761. };
  762. _picked.Paint += delegate(object s, PaintEventArgs e)
  763. {
  764. using (var p = new Pen(Color.FromArgb(90, 94, 234, 212)) { DashStyle = DashStyle.Dash })
  765. {
  766. e.Graphics.DrawRectangle(p, 1, 1, _picked.Width - 3, _picked.Height - 3);
  767. }
  768. };
  769. _spaceHint = new Label
  770. {
  771. Dock = DockStyle.Fill,
  772. ForeColor = Theme.Muted,
  773. BackColor = Theme.Card,
  774. TextAlign = ContentAlignment.MiddleLeft,
  775. Text = "插入 U 盘或移动硬盘后选择目录,无需手输路径。"
  776. };
  777. _keepOld = new CheckBox
  778. {
  779. Text = "同时删除该目录中超过 7 天的旧备份(U 盘建议不勾选)",
  780. ForeColor = Theme.Muted,
  781. Dock = DockStyle.Fill,
  782. BackColor = Theme.Card,
  783. FlatStyle = FlatStyle.Flat,
  784. TextAlign = ContentAlignment.MiddleLeft
  785. };
  786. _bar = new AiProgressBar
  787. {
  788. Dock = DockStyle.Fill,
  789. Minimum = 0,
  790. Maximum = 100
  791. };
  792. _progHint = new Label
  793. {
  794. Dock = DockStyle.Fill,
  795. ForeColor = Theme.Muted,
  796. BackColor = Theme.Card,
  797. TextAlign = ContentAlignment.MiddleLeft,
  798. Text = ""
  799. };
  800. var bakGo = new Panel { Dock = DockStyle.Fill, BackColor = Theme.Card };
  801. _btnBackup = MakeButton("开始备份", true);
  802. _btnBackup.Width = 148;
  803. _btnBackup.Height = 36;
  804. _btnBackup.Font = new Font("Microsoft YaHei UI", 11F, FontStyle.Bold);
  805. _btnBackup.Location = new Point(4, 4);
  806. _btnBackup.Click += OnBackup;
  807. _runStatus = new Label
  808. {
  809. AutoSize = false,
  810. Location = new Point(164, 10),
  811. Size = new Size(360, 24),
  812. Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
  813. ForeColor = Theme.Muted,
  814. BackColor = Theme.Card,
  815. Text = ""
  816. };
  817. bakGo.Controls.Add(_btnBackup);
  818. bakGo.Controls.Add(_runStatus);
  819. bakBody.Controls.Add(bakHead, 0, 0);
  820. bakBody.Controls.Add(_picked, 0, 1);
  821. bakBody.Controls.Add(_spaceHint, 0, 2);
  822. bakBody.Controls.Add(_keepOld, 0, 3);
  823. bakBody.Controls.Add(_bar, 0, 4);
  824. bakBody.Controls.Add(_progHint, 0, 5);
  825. bakBody.Controls.Add(bakGo, 0, 6);
  826. bakCard.Controls.Add(bakBody);
  827. var logCard = MakeCard();
  828. var logBody = MakeBody();
  829. logBody.RowCount = 2;
  830. logBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
  831. logBody.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
  832. logBody.Controls.Add(MakeHead("03", "运行控制台", "实时输出", null), 0, 0);
  833. _log = new TextBox
  834. {
  835. Multiline = true,
  836. ReadOnly = true,
  837. ScrollBars = ScrollBars.Vertical,
  838. BorderStyle = BorderStyle.None,
  839. Dock = DockStyle.Fill,
  840. BackColor = Theme.LogBg,
  841. ForeColor = Theme.LogText,
  842. Font = Theme.Mono,
  843. Text = "等待操作…"
  844. };
  845. logBody.Controls.Add(_log, 0, 1);
  846. logCard.Controls.Add(logBody);
  847. var left = new TableLayoutPanel();
  848. left.Dock = DockStyle.Fill;
  849. left.BackColor = Color.Transparent;
  850. left.ColumnCount = 1;
  851. left.RowCount = 2;
  852. left.RowStyles.Add(new RowStyle(SizeType.Percent, 54F));
  853. left.RowStyles.Add(new RowStyle(SizeType.Percent, 46F));
  854. left.Controls.Add(connCard, 0, 0);
  855. left.Controls.Add(bakCard, 0, 1);
  856. grid.Controls.Add(left, 0, 0);
  857. grid.Controls.Add(logCard, 1, 0);
  858. Controls.Add(grid);
  859. Controls.Add(header);
  860. _poll = new System.Windows.Forms.Timer();
  861. _poll.Interval = 1000;
  862. _poll.Tick += OnPoll;
  863. Load += delegate { LoadConfigToUi(); };
  864. FormClosing += OnClosing;
  865. }
  866. static string PrepareWorkDir(string exeDir)
  867. {
  868. string serverScript = Path.Combine(exeDir, "backup-db.ps1");
  869. string marker = Path.Combine(exeDir, Program.MarkerName);
  870. bool original = File.Exists(serverScript) && !File.Exists(marker);
  871. if (!original)
  872. {
  873. ExtractPackedFiles(exeDir);
  874. File.WriteAllText(marker, "packed", Encoding.ASCII);
  875. }
  876. return exeDir;
  877. }
  878. static void ExtractPackedFiles(string dir)
  879. {
  880. Assembly asm = Assembly.GetExecutingAssembly();
  881. foreach (string name in PackedFiles)
  882. {
  883. Stream src = asm.GetManifestResourceStream(name);
  884. if (src == null)
  885. {
  886. continue;
  887. }
  888. using (src)
  889. using (FileStream fs = File.Create(Path.Combine(dir, name)))
  890. {
  891. src.CopyTo(fs);
  892. }
  893. }
  894. }
  895. static CardPanel MakeCard()
  896. {
  897. return new CardPanel { Dock = DockStyle.Fill };
  898. }
  899. static TableLayoutPanel MakeBody()
  900. {
  901. var body = new TableLayoutPanel();
  902. body.Dock = DockStyle.Fill;
  903. body.ColumnCount = 1;
  904. body.BackColor = Theme.Card;
  905. body.Padding = new Padding(10, 6, 12, 8);
  906. body.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
  907. return body;
  908. }
  909. /// <summary>
  910. /// 卡片标题行:序号 + 标题 + 说明,右侧可放按钮。
  911. /// </summary>
  912. static Panel MakeHead(string step, string title, string hint, Control right)
  913. {
  914. var head = new Panel { Dock = DockStyle.Fill, BackColor = Theme.Card };
  915. var st = MakeStep(step);
  916. st.Location = new Point(2, 8);
  917. var t = SectionLabel(title, 38, 8);
  918. head.Controls.Add(st);
  919. head.Controls.Add(t);
  920. if (!string.IsNullOrEmpty(hint))
  921. {
  922. int titleW = TextRenderer.MeasureText(title, Theme.Section).Width;
  923. var h = new Label
  924. {
  925. Text = hint,
  926. Font = Theme.Caption,
  927. ForeColor = Theme.Muted,
  928. AutoSize = true,
  929. Location = new Point(46 + titleW, 12),
  930. BackColor = Theme.Card
  931. };
  932. head.Controls.Add(h);
  933. }
  934. if (right != null)
  935. {
  936. right.Anchor = AnchorStyles.Top | AnchorStyles.Right;
  937. head.Controls.Add(right);
  938. EventHandler place = delegate
  939. {
  940. right.Location = new Point(Math.Max(120, head.Width - right.Width - 4), 4);
  941. };
  942. head.Resize += place;
  943. place(head, EventArgs.Empty);
  944. }
  945. return head;
  946. }
  947. /// <summary>
  948. /// 标签在上、输入在下,两列等宽,避免右侧字段被挤没。
  949. /// </summary>
  950. static Panel MakeField(string caption, Control input)
  951. {
  952. var box = new Panel();
  953. box.Dock = DockStyle.Fill;
  954. box.BackColor = Theme.Card;
  955. box.Margin = new Padding(4, 2, 8, 2);
  956. box.Padding = new Padding(0, 0, 4, 2);
  957. var lb = new Label();
  958. lb.Text = caption;
  959. lb.ForeColor = Theme.Muted;
  960. lb.Font = Theme.Caption;
  961. lb.Dock = DockStyle.Top;
  962. lb.Height = 20;
  963. lb.BackColor = Theme.Card;
  964. lb.TextAlign = ContentAlignment.BottomLeft;
  965. input.Dock = DockStyle.Fill;
  966. box.Controls.Add(input);
  967. box.Controls.Add(lb);
  968. return box;
  969. }
  970. static Label MakeStep(string no)
  971. {
  972. return new Label
  973. {
  974. Text = no,
  975. Font = new Font("Segoe UI", 8.5F, FontStyle.Bold),
  976. ForeColor = Theme.Ink,
  977. BackColor = Theme.Cyan,
  978. Size = new Size(28, 22),
  979. Location = new Point(0, 6),
  980. TextAlign = ContentAlignment.MiddleCenter
  981. };
  982. }
  983. static void PaintMesh(object sender, PaintEventArgs e)
  984. {
  985. var c = sender as Control;
  986. if (c == null)
  987. {
  988. return;
  989. }
  990. using (var p = new Pen(Theme.Mesh))
  991. {
  992. for (int x = 0; x < c.Width; x += 42)
  993. {
  994. e.Graphics.DrawLine(p, x, 0, x, c.Height);
  995. }
  996. for (int y = 0; y < c.Height; y += 42)
  997. {
  998. e.Graphics.DrawLine(p, 0, y, c.Width, y);
  999. }
  1000. }
  1001. }
  1002. static Label SectionLabel(string text, int x, int y)
  1003. {
  1004. return new Label
  1005. {
  1006. Text = text,
  1007. Font = Theme.Section,
  1008. ForeColor = Theme.Text,
  1009. AutoSize = true,
  1010. Location = new Point(x, y),
  1011. BackColor = Theme.Card
  1012. };
  1013. }
  1014. static Label FieldLabel(string text)
  1015. {
  1016. return new Label
  1017. {
  1018. Text = text,
  1019. ForeColor = Theme.Muted,
  1020. Dock = DockStyle.Fill,
  1021. TextAlign = ContentAlignment.MiddleLeft,
  1022. BackColor = Theme.Card
  1023. };
  1024. }
  1025. static TextBox MakeInput()
  1026. {
  1027. return new TextBox
  1028. {
  1029. Dock = DockStyle.Fill,
  1030. BorderStyle = BorderStyle.FixedSingle,
  1031. BackColor = Theme.Input,
  1032. ForeColor = Theme.Text,
  1033. Margin = new Padding(0)
  1034. };
  1035. }
  1036. static Button MakeButton(string text, bool primary)
  1037. {
  1038. var b = new Button();
  1039. b.Text = text;
  1040. b.Width = 110;
  1041. b.Height = 32;
  1042. b.FlatStyle = FlatStyle.Flat;
  1043. b.Cursor = Cursors.Hand;
  1044. if (primary)
  1045. {
  1046. b.FlatAppearance.BorderSize = 0;
  1047. b.BackColor = Theme.CyanDeep;
  1048. b.ForeColor = Theme.Ink;
  1049. b.MouseEnter += delegate { if (b.Enabled) { b.BackColor = Theme.Cyan; } };
  1050. b.MouseLeave += delegate { b.BackColor = Theme.CyanDeep; };
  1051. }
  1052. else
  1053. {
  1054. b.FlatAppearance.BorderSize = 1;
  1055. b.FlatAppearance.BorderColor = Color.FromArgb(70, 255, 255, 255);
  1056. b.BackColor = Color.FromArgb(18, 22, 32);
  1057. b.ForeColor = Theme.Text;
  1058. b.MouseEnter += delegate { if (b.Enabled) { b.BackColor = Color.FromArgb(28, 36, 52); } };
  1059. b.MouseLeave += delegate { b.BackColor = Color.FromArgb(18, 22, 32); };
  1060. }
  1061. return b;
  1062. }
  1063. static Button MakeGoldButton(string text)
  1064. {
  1065. var b = new Button();
  1066. b.Text = text;
  1067. b.Width = 110;
  1068. b.Height = 32;
  1069. b.FlatStyle = FlatStyle.Flat;
  1070. b.FlatAppearance.BorderSize = 0;
  1071. b.BackColor = Theme.Cyan;
  1072. b.ForeColor = Theme.Ink;
  1073. b.Cursor = Cursors.Hand;
  1074. b.MouseEnter += delegate { if (b.Enabled) { b.BackColor = Theme.CyanDeep; } };
  1075. b.MouseLeave += delegate { b.BackColor = Theme.Cyan; };
  1076. return b;
  1077. }
  1078. DbConfig ReadUiConfig()
  1079. {
  1080. var cfg = new DbConfig();
  1081. cfg.Host = (_host.Text ?? "").Trim();
  1082. int port;
  1083. if (!int.TryParse((_port.Text ?? "").Trim(), out port))
  1084. {
  1085. port = 3306;
  1086. }
  1087. cfg.Port = port;
  1088. cfg.Database = (_database.Text ?? "").Trim();
  1089. cfg.User = (_user.Text ?? "").Trim();
  1090. cfg.Password = _password.Text ?? "";
  1091. return cfg;
  1092. }
  1093. void LoadConfigToUi()
  1094. {
  1095. DbConfig cfg = DbConfigStore.Load(_workDir);
  1096. _host.Text = cfg.Host;
  1097. _port.Text = cfg.Port.ToString();
  1098. _database.Text = cfg.Database;
  1099. _user.Text = cfg.User;
  1100. _password.Text = cfg.Password;
  1101. }
  1102. void OnTest(object sender, EventArgs e)
  1103. {
  1104. _btnTest.Enabled = false;
  1105. SetCfg("正在测试连接,请稍候…", Theme.Warn);
  1106. DbConfig cfg = ReadUiConfig();
  1107. ThreadPool.QueueUserWorkItem(delegate
  1108. {
  1109. try
  1110. {
  1111. string msg = MysqlUtil.TestConnection(cfg);
  1112. BeginInvoke(new Action(delegate
  1113. {
  1114. SetCfg(msg, Theme.Ok);
  1115. _btnTest.Enabled = true;
  1116. }));
  1117. }
  1118. catch (Exception ex)
  1119. {
  1120. BeginInvoke(new Action(delegate
  1121. {
  1122. SetCfg(ex.Message, Theme.Bad);
  1123. _btnTest.Enabled = true;
  1124. }));
  1125. }
  1126. });
  1127. }
  1128. void OnSave(object sender, EventArgs e)
  1129. {
  1130. try
  1131. {
  1132. DbConfig cfg = ReadUiConfig();
  1133. DbConfigStore.Save(_workDir, cfg);
  1134. SetCfg("已保存 MySQL 配置", Theme.Ok);
  1135. }
  1136. catch (Exception ex)
  1137. {
  1138. SetCfg(ex.Message, Theme.Bad);
  1139. }
  1140. }
  1141. void OnBrowse(object sender, EventArgs e)
  1142. {
  1143. using (var dlg = new FolderBrowserDialog())
  1144. {
  1145. dlg.Description = "选择备份文件保存位置(可选手动插入的 U 盘或移动硬盘)";
  1146. dlg.ShowNewFolderButton = true;
  1147. dlg.RootFolder = Environment.SpecialFolder.MyComputer;
  1148. if (!string.IsNullOrEmpty(_selectedPath) && Directory.Exists(_selectedPath))
  1149. {
  1150. dlg.SelectedPath = _selectedPath;
  1151. }
  1152. if (dlg.ShowDialog(this) != DialogResult.OK)
  1153. {
  1154. return;
  1155. }
  1156. try
  1157. {
  1158. _selectedPath = GetSafePath(dlg.SelectedPath);
  1159. _picked.Text = " " + _selectedPath;
  1160. _picked.ForeColor = Theme.Text;
  1161. long free = GetFreeBytes(_selectedPath);
  1162. if (free > 0)
  1163. {
  1164. _spaceHint.Text = "该磁盘剩余 " + Gb(free) + " GB。";
  1165. }
  1166. SetRun("已选择保存位置", Theme.Ok);
  1167. }
  1168. catch (Exception ex)
  1169. {
  1170. SetRun(ex.Message, Theme.Bad);
  1171. }
  1172. }
  1173. }
  1174. void OnBackup(object sender, EventArgs e)
  1175. {
  1176. if (string.IsNullOrWhiteSpace(_selectedPath))
  1177. {
  1178. SetRun("请先点「浏览文件夹」选择保存位置", Theme.Bad);
  1179. return;
  1180. }
  1181. string script = Path.Combine(_workDir, "backup-db.ps1");
  1182. if (!File.Exists(script))
  1183. {
  1184. SetRun("找不到 backup-db.ps1", Theme.Bad);
  1185. return;
  1186. }
  1187. try
  1188. {
  1189. DbConfig cfg = ReadUiConfig();
  1190. DbConfigStore.Save(_workDir, cfg);
  1191. string dir = GetSafePath(_selectedPath);
  1192. long free = GetFreeBytes(dir);
  1193. _dbBytes = MysqlUtil.GetDbSizeBytes(cfg);
  1194. if (_dbBytes > 0 && free > 0 && free < (long)(_dbBytes * 1.2))
  1195. {
  1196. throw new Exception("目标盘剩余空间不足:约需 " + Gb((long)(_dbBytes * 1.2)) +
  1197. " GB,当前剩余 " + Gb(free) + " GB。请换更大的 U 盘/硬盘。");
  1198. }
  1199. if (!Directory.Exists(dir))
  1200. {
  1201. Directory.CreateDirectory(dir);
  1202. }
  1203. int keep = _keepOld.Checked ? 7 : 0;
  1204. string powershell = Path.Combine(Environment.SystemDirectory, @"WindowsPowerShell\v1.0\powershell.exe");
  1205. if (!File.Exists(powershell))
  1206. {
  1207. powershell = "powershell.exe";
  1208. }
  1209. _outFile = Path.Combine(Path.GetTempPath(), "mes_backup_ui_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".log");
  1210. string errFile = _outFile + ".err";
  1211. TryDelete(_outFile);
  1212. TryDelete(errFile);
  1213. var psi = new ProcessStartInfo();
  1214. psi.FileName = powershell;
  1215. psi.Arguments = "-NoProfile -STA -ExecutionPolicy Bypass -File \"" + script +
  1216. "\" -BackupDir \"" + dir + "\" -KeepDaysParam " + keep;
  1217. psi.WorkingDirectory = _workDir;
  1218. psi.UseShellExecute = false;
  1219. psi.CreateNoWindow = true;
  1220. psi.RedirectStandardOutput = true;
  1221. psi.RedirectStandardError = true;
  1222. psi.StandardOutputEncoding = Encoding.UTF8;
  1223. psi.StandardErrorEncoding = Encoding.UTF8;
  1224. _backupProc = new Process();
  1225. _backupProc.StartInfo = psi;
  1226. _backupProc.EnableRaisingEvents = true;
  1227. _backupProc.OutputDataReceived += OnBackupOutput;
  1228. _backupProc.ErrorDataReceived += OnBackupOutput;
  1229. if (!_backupProc.Start())
  1230. {
  1231. throw new Exception("无法启动备份进程");
  1232. }
  1233. _backupProc.BeginOutputReadLine();
  1234. _backupProc.BeginErrorReadLine();
  1235. _running = true;
  1236. _startedAt = DateTime.Now;
  1237. _selectedPath = dir;
  1238. _btnBackup.Enabled = false;
  1239. _bar.Style = ProgressBarStyle.Marquee;
  1240. _bar.Value = 0;
  1241. SetLog(new[] { "开始备份到: " + dir });
  1242. if (_dbBytes > 0)
  1243. {
  1244. AppendLog("库数据约 " + Gb(_dbBytes) + " GB,采用边导出边压缩,请耐心等待,勿拔盘");
  1245. }
  1246. SetRun("正在备份,数据量大时可能需要较长时间...", Theme.Warn);
  1247. _poll.Start();
  1248. }
  1249. catch (Exception ex)
  1250. {
  1251. SetRun(ex.Message, Theme.Bad);
  1252. _btnBackup.Enabled = true;
  1253. }
  1254. }
  1255. void OnBackupOutput(object sender, DataReceivedEventArgs e)
  1256. {
  1257. if (string.IsNullOrEmpty(e.Data))
  1258. {
  1259. return;
  1260. }
  1261. BeginInvoke(new Action(delegate { AppendLog(e.Data); }));
  1262. }
  1263. void OnPoll(object sender, EventArgs e)
  1264. {
  1265. if (!_running)
  1266. {
  1267. return;
  1268. }
  1269. long written;
  1270. string fileName;
  1271. GetWrittenBackup(_selectedPath, out written, out fileName);
  1272. if (_dbBytes > 0 && written > 1024)
  1273. {
  1274. _bar.Style = ProgressBarStyle.Continuous;
  1275. int pct = (int)Math.Max(2, Math.Min(99, written * 100 / _dbBytes));
  1276. _bar.Value = pct;
  1277. }
  1278. _progHint.Text = "已写入 " + Mb(written) + " MB" + (string.IsNullOrEmpty(fileName) ? "" : "(" + fileName + ")") + ",请勿拔盘";
  1279. Process p = _backupProc;
  1280. bool exited = false;
  1281. int code = -1;
  1282. if (p == null)
  1283. {
  1284. exited = true;
  1285. }
  1286. else
  1287. {
  1288. try
  1289. {
  1290. p.Refresh();
  1291. exited = p.HasExited;
  1292. if (exited)
  1293. {
  1294. try { p.WaitForExit(500); } catch { }
  1295. try { code = p.ExitCode; } catch { code = -1; }
  1296. }
  1297. }
  1298. catch
  1299. {
  1300. exited = true;
  1301. }
  1302. }
  1303. if (!exited)
  1304. {
  1305. return;
  1306. }
  1307. _poll.Stop();
  1308. _running = false;
  1309. _btnBackup.Enabled = true;
  1310. _bar.Style = ProgressBarStyle.Continuous;
  1311. bool fileOk = TestBackupFileOk(_selectedPath, _startedAt);
  1312. bool success = (code == 0) || fileOk;
  1313. if (success)
  1314. {
  1315. _bar.Value = 100;
  1316. GetWrittenBackup(_selectedPath, out written, out fileName);
  1317. _progHint.Text = "完成,文件约 " + Mb(written) + " MB:" + (fileName ?? "");
  1318. AppendLog("备份完成");
  1319. SetRun("备份完成", Theme.Ok);
  1320. }
  1321. else
  1322. {
  1323. string codeText = code < 0 ? "未知" : code.ToString();
  1324. string err = "备份失败,退出码 " + codeText;
  1325. AppendLog(err);
  1326. SetRun(err, Theme.Bad);
  1327. }
  1328. _backupProc = null;
  1329. }
  1330. void OnClosing(object sender, FormClosingEventArgs e)
  1331. {
  1332. if (_running)
  1333. {
  1334. DialogResult r = MessageBox.Show(this,
  1335. "备份正在进行,关闭窗口会中断备份。确定退出吗?",
  1336. "MES 数据库备份",
  1337. MessageBoxButtons.YesNo,
  1338. MessageBoxIcon.Warning);
  1339. if (r != DialogResult.Yes)
  1340. {
  1341. e.Cancel = true;
  1342. return;
  1343. }
  1344. }
  1345. _poll.Stop();
  1346. Process p = _backupProc;
  1347. _backupProc = null;
  1348. if (p != null)
  1349. {
  1350. try
  1351. {
  1352. if (!p.HasExited)
  1353. {
  1354. KillProcessTree(p.Id);
  1355. }
  1356. }
  1357. catch { }
  1358. try { p.Dispose(); } catch { }
  1359. }
  1360. }
  1361. static void GetWrittenBackup(string dir, out long bytes, out string name)
  1362. {
  1363. bytes = 0;
  1364. name = "";
  1365. if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir))
  1366. {
  1367. return;
  1368. }
  1369. FileInfo pick = null;
  1370. try
  1371. {
  1372. var files = new List<FileInfo>(new DirectoryInfo(dir).GetFiles("mes_cloud_*.sql.gz*"));
  1373. files.Sort(delegate(FileInfo a, FileInfo b) { return b.LastWriteTime.CompareTo(a.LastWriteTime); });
  1374. foreach (FileInfo f in files)
  1375. {
  1376. if (f.Name.IndexOf(".tmp", StringComparison.OrdinalIgnoreCase) < 0)
  1377. {
  1378. pick = f;
  1379. break;
  1380. }
  1381. }
  1382. if (pick == null && files.Count > 0)
  1383. {
  1384. pick = files[0];
  1385. }
  1386. }
  1387. catch { }
  1388. if (pick != null)
  1389. {
  1390. name = pick.Name;
  1391. bytes = pick.Length;
  1392. }
  1393. }
  1394. static bool TestBackupFileOk(string dir, DateTime started)
  1395. {
  1396. if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir))
  1397. {
  1398. return false;
  1399. }
  1400. FileInfo gz = null;
  1401. try
  1402. {
  1403. foreach (FileInfo f in new DirectoryInfo(dir).GetFiles("mes_cloud_*.sql.gz"))
  1404. {
  1405. if (gz == null || f.LastWriteTime > gz.LastWriteTime)
  1406. {
  1407. gz = f;
  1408. }
  1409. }
  1410. }
  1411. catch { return false; }
  1412. if (gz == null || gz.Length <= 100)
  1413. {
  1414. return false;
  1415. }
  1416. if (gz.LastWriteTime < started.AddMinutes(-2))
  1417. {
  1418. return false;
  1419. }
  1420. return true;
  1421. }
  1422. static string GetSafePath(string path)
  1423. {
  1424. if (string.IsNullOrWhiteSpace(path))
  1425. {
  1426. throw new Exception("请先选择保存位置");
  1427. }
  1428. string p = path.Trim().Trim('"');
  1429. if (!Regex.IsMatch(p, @"^[A-Za-z]:\\"))
  1430. {
  1431. throw new Exception("只允许本机磁盘路径");
  1432. }
  1433. string full = Path.GetFullPath(p);
  1434. if (Regex.IsMatch(full, @"^[A-Za-z]:\\?$"))
  1435. {
  1436. return char.ToUpperInvariant(full[0]) + @":\";
  1437. }
  1438. return full.TrimEnd('\\');
  1439. }
  1440. static long GetFreeBytes(string path)
  1441. {
  1442. try
  1443. {
  1444. string full = GetSafePath(path);
  1445. string root = Path.GetPathRoot(full);
  1446. var di = new DriveInfo(root);
  1447. if (di.IsReady)
  1448. {
  1449. return di.AvailableFreeSpace;
  1450. }
  1451. }
  1452. catch { }
  1453. return 0;
  1454. }
  1455. static void KillProcessTree(int pid)
  1456. {
  1457. try
  1458. {
  1459. var psi = new ProcessStartInfo();
  1460. psi.FileName = "taskkill";
  1461. psi.Arguments = "/PID " + pid + " /T /F";
  1462. psi.CreateNoWindow = true;
  1463. psi.UseShellExecute = false;
  1464. using (Process k = Process.Start(psi))
  1465. {
  1466. if (k != null)
  1467. {
  1468. k.WaitForExit(5000);
  1469. }
  1470. }
  1471. }
  1472. catch { }
  1473. }
  1474. static void TryDelete(string path)
  1475. {
  1476. try { if (File.Exists(path)) { File.Delete(path); } } catch { }
  1477. }
  1478. static string Gb(long n)
  1479. {
  1480. return (n / 1073741824.0).ToString("0.00");
  1481. }
  1482. static string Mb(long n)
  1483. {
  1484. return (n / 1048576.0).ToString("0.0");
  1485. }
  1486. void SetCfg(string text, Color color)
  1487. {
  1488. _cfgStatus.Text = text ?? "";
  1489. _cfgStatus.ForeColor = color;
  1490. }
  1491. void SetRun(string text, Color color)
  1492. {
  1493. _runStatus.Text = text ?? "";
  1494. _runStatus.ForeColor = color;
  1495. }
  1496. void SetLog(string[] lines)
  1497. {
  1498. _log.Text = (lines != null && lines.Length > 0) ? string.Join(Environment.NewLine, lines) : "等待操作…";
  1499. _log.SelectionStart = _log.TextLength;
  1500. _log.ScrollToCaret();
  1501. }
  1502. void AppendLog(string line)
  1503. {
  1504. if (_log.Text == "等待操作…" || _log.Text == "等待操作...")
  1505. {
  1506. _log.Text = "";
  1507. }
  1508. if (_log.TextLength > 60000)
  1509. {
  1510. _log.Text = _log.Text.Substring(_log.TextLength - 40000);
  1511. }
  1512. _log.AppendText(line + Environment.NewLine);
  1513. }
  1514. }
  1515. }