FormMain.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Windows.Forms;
  8. using System.Xml.Linq;
  9. using Newtonsoft.Json;
  10. namespace MSBuild
  11. {
  12. public partial class FormMain : Form
  13. {
  14. #region 变量
  15. /// <summary>
  16. /// 配置信息保存路径
  17. /// </summary>
  18. private static readonly string Dir = AppDomain.CurrentDomain.BaseDirectory + @"SaveDir\";
  19. /// <summary>
  20. /// 需要编译的文件路径
  21. /// </summary>
  22. private List<string> _listProject;
  23. /// <summary>
  24. /// 替换dll
  25. /// </summary>
  26. private string[] _listDll;
  27. /// <summary>
  28. /// 获取编译工具路径
  29. /// </summary>
  30. private string _versionUrl;
  31. /// <summary>
  32. /// 获取编译模式
  33. /// </summary>
  34. private string _configure;
  35. /// <summary>
  36. /// 指定文件夹
  37. /// </summary>
  38. private string _saveUrl;
  39. /// <summary>
  40. /// 编译的日志
  41. /// </summary>
  42. private string _buildLog;
  43. /// <summary>
  44. /// 记录替换过程中发生的信息
  45. /// </summary>
  46. private string _replaceText;
  47. /// <summary>
  48. /// 替换的线程
  49. /// </summary>
  50. private readonly BackgroundWorker _replaceWorker = new BackgroundWorker();
  51. /// <summary>
  52. /// 编译的线程
  53. /// </summary>
  54. private readonly BackgroundWorker _buildWorker = new BackgroundWorker();
  55. #endregion
  56. #region 窗体方法
  57. public FormMain()
  58. {
  59. InitializeComponent();
  60. }
  61. /// <summary>
  62. /// 响应窗体的Load事件
  63. /// </summary>
  64. private void FormMain_Load(object sender, EventArgs e)
  65. {
  66. projectFileText.ReadOnly = dllFileText.ReadOnly = outputText.ReadOnly = buildText.ReadOnly = true;
  67. buttonReplace.Enabled = false;
  68. // 获取编译工具
  69. BindVersion();
  70. // 获取编译模式
  71. BindModel();
  72. // 设置线程属性
  73. _replaceWorker.WorkerReportsProgress = _buildWorker.WorkerReportsProgress = true; // 进度更新
  74. // 设置线程处理的对象
  75. _replaceWorker.DoWork += _replaceWorker_DoWork;
  76. _replaceWorker.ProgressChanged += _replaceWorker_ProgressChanged;
  77. _replaceWorker.RunWorkerCompleted += _replaceWorker_RunWorkerCompleted;
  78. _buildWorker.DoWork += _buildWorker_DoWork;
  79. _buildWorker.ProgressChanged += _buildWorker_ProgressChanged;
  80. _buildWorker.RunWorkerCompleted += _buildWorker_RunWorkerCompleted;
  81. }
  82. #endregion
  83. #region 对象方法
  84. /// <summary>
  85. /// 打开文件文件夹
  86. /// </summary>
  87. private void buttonOpenProject_Click(object sender, EventArgs e)
  88. {
  89. var dialog = new FolderBrowserDialog { Description = @"请选择需要编译的文件路径!" };
  90. bool? result = dialog.ShowDialog() == DialogResult.OK;
  91. if (result == true)
  92. {
  93. projectFileText.Text = dialog.SelectedPath;
  94. }
  95. }
  96. /// <summary>
  97. /// 打开替换Dll文件夹
  98. /// </summary>
  99. private void buttonOpenDll_Click(object sender, EventArgs e)
  100. {
  101. var dialog = new FolderBrowserDialog { Description = @"请选择替换Dll的文件路径!" };
  102. bool? result = dialog.ShowDialog() == DialogResult.OK;
  103. if (result == true)
  104. {
  105. dllFileText.Text = dialog.SelectedPath;
  106. }
  107. }
  108. /// <summary>
  109. /// 查找需要替换的文件
  110. /// </summary>
  111. private void buttonAnalysis_Click(object sender, EventArgs e)
  112. {
  113. /* 清空显示 */
  114. projectText.Text = "";
  115. // 判断编译路径是否存在
  116. if (string.IsNullOrWhiteSpace(projectFileText.Text))
  117. {
  118. MessageBox.Show(@"编译文件夹路径不能为空!");
  119. return;
  120. }
  121. // 判断文件夹是否存在
  122. if (!Directory.Exists(projectFileText.Text))
  123. {
  124. MessageBox.Show(@"编译文件夹路径不存在");
  125. return;
  126. }
  127. // 判断替换路径是否存在
  128. if (string.IsNullOrWhiteSpace(dllFileText.Text))
  129. {
  130. MessageBox.Show(@"替换路径不能为空!");
  131. return;
  132. }
  133. // 判断替换文件夹是否存在
  134. if (!Directory.Exists(dllFileText.Text))
  135. {
  136. MessageBox.Show(@"替换文件夹不存在!");
  137. return;
  138. }
  139. // 获取编译路径下的所有.csproj文件
  140. _listProject = Directory.GetFiles(projectFileText.Text, "*.csproj", SearchOption.AllDirectories).ToList();
  141. // 若不存在,则提示
  142. if (_listProject.Count == 0)
  143. {
  144. MessageBox.Show(@"不存在需要编译的文件");
  145. return;
  146. }
  147. foreach (var project in _listProject)
  148. {
  149. projectText.Text += project + Environment.NewLine;
  150. }
  151. // 获取替换dll
  152. _listDll = Directory.GetFiles(dllFileText.Text, "*.dll", SearchOption.AllDirectories);
  153. if (_listDll.Length == 0)
  154. {
  155. MessageBox.Show(@"替换路径下,没有发现可以替换的DLL!");
  156. buttonReplace.Enabled = false;
  157. }
  158. buttonReplace.Enabled = true;
  159. }
  160. /// <summary>
  161. /// 替换引用
  162. /// </summary>
  163. private void buttonReplace_Click(object sender, EventArgs e)
  164. {
  165. // 替换路径不能为空
  166. if (string.IsNullOrWhiteSpace(dllFileText.Text))
  167. {
  168. MessageBox.Show(@"替换路径不能为空!");
  169. return;
  170. }
  171. // 判断是否正在异步执行
  172. if (!_replaceWorker.IsBusy)
  173. {
  174. _replaceWorker.RunWorkerAsync();
  175. }
  176. }
  177. /// <summary>
  178. /// 选择输出文件夹
  179. /// </summary>
  180. private void buttonOutputFile_Click(object sender, EventArgs e)
  181. {
  182. var dialog = new FolderBrowserDialog { Description = @"请选择输出的文件路径!" };
  183. bool? result = dialog.ShowDialog() == DialogResult.OK;
  184. if (result == true)
  185. {
  186. outputText.Text = dialog.SelectedPath;
  187. }
  188. }
  189. /// <summary>
  190. /// 编译
  191. /// </summary>
  192. private void buttonBuild_Click(object sender, EventArgs e)
  193. {
  194. /* 清空显示 */
  195. buildText.Text = "";
  196. // 判断编译源是否为空
  197. if (_listProject == null || _listProject.Count == 0)
  198. {
  199. MessageBox.Show(@"请先查找!");
  200. return;
  201. }
  202. // 判断输出路径是否存在
  203. if (!Directory.Exists(outputText.Text))
  204. {
  205. MessageBox.Show(@"指定输出路径不存在");
  206. return;
  207. }
  208. // 获取编译工具路径
  209. _versionUrl = ((ItemModel)versionCom.SelectedItem).Value;
  210. // 获取编译模式
  211. _configure = ((ItemModel)configureBox.SelectedItem).Value;
  212. // 指定文件夹
  213. _saveUrl = outputText.Text;
  214. // 判断是否正在执行异步操作
  215. if (!_buildWorker.IsBusy && !_replaceWorker.IsBusy)
  216. {
  217. _buildWorker.RunWorkerAsync();
  218. }
  219. }
  220. /// <summary>
  221. /// 保存配置的事件
  222. /// </summary>
  223. private void SaveConfigure_Click(object sender, EventArgs e)
  224. {
  225. //获取当前配置
  226. var config = GetConfigModel();
  227. //保存路径不存在时,创建
  228. if (!Directory.Exists(Dir))
  229. {
  230. Directory.CreateDirectory(Dir);
  231. }
  232. //若文件不存在则创建
  233. var filePath = Dir + "config.txt";
  234. var file = new StreamWriter(filePath, false);
  235. //写入
  236. file.Write(JsonConvert.SerializeObject(config));
  237. //关闭
  238. file.Close();
  239. file.Dispose();
  240. //提示
  241. MessageBox.Show(@"保存成功");
  242. }
  243. /// <summary>
  244. /// 读取配置的事件
  245. /// </summary>
  246. private void ReadConfigure_Click(object sender, EventArgs e)
  247. {
  248. try
  249. {
  250. // 获取文件内容
  251. if (!File.Exists(Dir + "config.txt"))
  252. {
  253. return;
  254. }
  255. var file = new StreamReader(Dir + "config.txt");
  256. var str = file.ReadToEnd();
  257. file.Close();
  258. file.Dispose();
  259. if (string.IsNullOrWhiteSpace(str))
  260. {
  261. return;
  262. }
  263. // 反序列化
  264. var config = JsonConvert.DeserializeObject<ConfigureModel>(str);
  265. if (config == null)
  266. {
  267. return;
  268. }
  269. // 赋值
  270. projectFileText.Text = config.Url;
  271. dllFileText.Text = config.PathUrl;
  272. outputText.Text = config.OutputUrl;
  273. versionCom.SelectedValue = config.VersionUlr;
  274. configureBox.SelectedValue = config.ModelKey;
  275. }
  276. catch (Exception exception)
  277. {
  278. MessageBox.Show(exception.Message);
  279. }
  280. }
  281. /// <summary>
  282. /// 响应_replaceWorker的DoWork事件
  283. /// </summary>
  284. private void _replaceWorker_DoWork(object sender, DoWorkEventArgs e)
  285. {
  286. // 清空文本
  287. _replaceText = "";
  288. // 获取替换的dll
  289. var dllFiles = _listDll
  290. .Select(file => new DirectoryInfo(file)).Select(path => new AnalysisModel { NewName = path.Name, NewPath = path.FullName })
  291. .ToList();
  292. // 循环获取替换
  293. var log = "";
  294. var count = _listProject.Count;
  295. for (var index = count - 1; index >= 0; index--)
  296. {
  297. _replaceWorker.ReportProgress(int.Parse(Math.Ceiling((count - index) / count * 100.0).ToString(CultureInfo.CurrentCulture)));
  298. var project = _listProject[index];
  299. // 获取项目名称
  300. var projectName = project.Substring(project.LastIndexOf('\\') + 1);
  301. var name = projectName.Substring(0, projectName.LastIndexOf('.'));
  302. //获取.csproj文件中的所有非系统引用
  303. var doc = XDocument.Load(project);
  304. var element = (doc.Root.Elements())
  305. .First(x => x.Name.LocalName == "ItemGroup");
  306. if (element == null)
  307. {
  308. log += name + ":项目中没有引用第三方不需替换。" + Environment.NewLine;
  309. _replaceText += name + "替换失败" + Environment.NewLine;
  310. continue;
  311. }
  312. var reference = element.Elements()
  313. .Where(x => x.Name.LocalName == "Reference")
  314. .ToList();
  315. var projectLists = reference
  316. .Select(el => new AnalysisModel() { OldName = el.Attribute("Include").Value, OldPath = el.Value })
  317. .Where(x => !string.IsNullOrWhiteSpace(x.OldPath))
  318. .ToList();
  319. if (projectLists.Count == 0)
  320. {
  321. log += name + ":项目中没有引用第三方不需替换。" + Environment.NewLine;
  322. _replaceText += name + "替换失败" + Environment.NewLine;
  323. continue;
  324. }
  325. //根据获取的引用查找替换文件夹中的引用
  326. var list = (from projectList in projectLists
  327. let sp = projectList.OldName.Split(',')
  328. let y = sp.Length == 0 ? projectList.OldName : sp[0]
  329. let split = y.Split(new[] { ".v" }, StringSplitOptions.None)
  330. let x = split.Length == 1 ? y : split[1] == "1" ? y : split[0]
  331. let z = split.Length < 2 || split[1].IndexOf('.') + 1 == split[1].Length - 1 ? "" : split[1].Substring(split[1].IndexOf('.', split[1].IndexOf('.') + 1))
  332. let contain = dllFiles.Where(a => a.NewName.Contains(x)).ToList()
  333. let contains = z == "" ? contain : contain.Where(b => b.NewName.Contains(z)).ToList()
  334. where contains.Count != 0
  335. select new AnalysisModel
  336. {
  337. OldName = projectList.OldName,
  338. OldPath = projectList.OldPath,
  339. NewName = contains[0].NewName,
  340. NewPath = contains[0].NewPath
  341. })
  342. .ToList();
  343. var listExcept = (from projectList in projectLists
  344. let sp = projectList.OldName.Split(',')
  345. let y = sp.Length == 0 ? projectList.OldName : sp[0]
  346. let split = y.Split(new[] { ".v" }, StringSplitOptions.None)
  347. let x = split.Length == 0 ? projectList.OldName : split[0]
  348. let contain = dllFiles.Where(z => z.NewName.Contains(x)).ToList()
  349. where contain.Count == 0
  350. select new AnalysisModel
  351. {
  352. OldName = y
  353. })
  354. .ToList();
  355. if (list.Count == 0)
  356. {
  357. log += name + ":所引用的第三方dll,在引用文件夹中全部不存在" + Environment.NewLine;
  358. _replaceText += name + "替换失败" + Environment.NewLine;
  359. _listProject.Remove(project); //不进行编译
  360. continue;
  361. }
  362. if (listExcept.Count != 0)
  363. {
  364. var listDll = string.Join(",", listExcept.Select(x => x.OldName));
  365. log += name + ":所引用的第三方dll" + listDll + ",在引用文件夹中不存在" + Environment.NewLine;
  366. _replaceText += name + "替换失败" + Environment.NewLine;
  367. _listProject.Remove(project); //不进行编译
  368. continue;
  369. }
  370. //获取.csproj文件中的所有非系统引用
  371. var elements = element.Elements().Where(x => !string.IsNullOrWhiteSpace(x.Value)).ToList();
  372. for (var i = elements.Count - 1; i >= 0; i--)
  373. {
  374. var elem = elements[i];
  375. var old = list.Where(x => x.OldName == elem.Attribute("Include").Value).ToList();
  376. if (old.Count == 0) continue;
  377. //添加新节点
  378. XNamespace nameNamespace = element.Name.NamespaceName;
  379. var referenceName = new XElement(nameNamespace + "Reference");
  380. var newName = old[0].NewName.Split(new[] { ".dll" }, StringSplitOptions.None);
  381. referenceName.SetAttributeValue("Include", newName.Length == 0 ? old[0].NewName : newName[0]);
  382. referenceName.SetElementValue(nameNamespace + "HintPath", old[0].NewPath);
  383. element.AddFirst(referenceName);
  384. //移除当前节点
  385. elem.Remove();
  386. }
  387. doc.Save(project, SaveOptions.OmitDuplicateNamespaces);
  388. _replaceText += name + "替换成功" + Environment.NewLine;
  389. }
  390. //保存路径不存在时,创建
  391. if (!Directory.Exists(Dir))
  392. {
  393. Directory.CreateDirectory(Dir);
  394. }
  395. //若文件不存在则创建
  396. var filePath = Dir + "log.txt";
  397. var logFile = new StreamWriter(filePath, false);
  398. //写入
  399. logFile.Write(log);
  400. //关闭
  401. logFile.Close();
  402. logFile.Dispose();
  403. //提示
  404. _replaceText += "如有替换失败,请查看log.txt日志";
  405. }
  406. /// <summary>
  407. /// 响应_replaceWorker的ProgressChanged事件
  408. /// </summary>
  409. private void _replaceWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
  410. {
  411. replaceProgress.Value = e.ProgressPercentage;
  412. }
  413. /// <summary>
  414. /// 响应_replaceWorker的RunWorkerCompleted事件
  415. /// </summary>
  416. private void _replaceWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
  417. {
  418. MessageBox.Show(e.Cancelled ? @"进程已被取消:" + replaceProgress.Value + "%" : @"进程执行完成:" + replaceProgress.Value + "%");
  419. replaceProgress.Value = 0;
  420. projectText.Text = _replaceText;
  421. }
  422. /// <summary>
  423. /// 响应_buildWorker的DoWork事件
  424. /// </summary>
  425. private void _buildWorker_DoWork(object sender, DoWorkEventArgs e)
  426. {
  427. //循环编译
  428. string errLog = "";
  429. var count = _listProject.Count;
  430. for (var index = 0; index < _listProject.Count; index++)
  431. {
  432. var project = _listProject[index];
  433. // 获取编译文件路径
  434. var buildUrl = project;
  435. // 获取输出路径,不存在则新建,存在则删除文件夹中的文件
  436. var outputUrl = buildUrl.Substring(0, buildUrl.LastIndexOf('\\')) + "\\bin\\" + _configure;
  437. if (!Directory.Exists(outputUrl))
  438. {
  439. Directory.CreateDirectory(outputUrl);
  440. }
  441. else
  442. {
  443. DeleteDir(outputUrl);
  444. }
  445. // 定义编译脚本
  446. var cmd = "";
  447. cmd += "@echo off";
  448. cmd += "\r\n";
  449. cmd += "\"" + _versionUrl + "\" " + "\"" + buildUrl + "\" " + "/t:rebuild /p:Configuration=" + _configure + " /p:OutDir=" + "\"" + outputUrl + "\"";
  450. // 编译
  451. string output;
  452. ComHelper.RunCmd(cmd, out output);
  453. _buildLog = output + Environment.NewLine;
  454. // 复制文件到指定文件夹
  455. var file = buildUrl.Substring(buildUrl.LastIndexOf('\\'));
  456. var fileName = file.Substring(0, file.LastIndexOf('.')) + ".dll";
  457. if (!File.Exists(outputUrl + '\\' + fileName))
  458. {
  459. fileName = file.Substring(0, file.LastIndexOf('.')) + ".exe";
  460. }
  461. try
  462. {
  463. File.Copy(outputUrl + '\\' + fileName, _saveUrl + '\\' + fileName, true);
  464. }
  465. catch
  466. {
  467. errLog += output;
  468. }
  469. _buildWorker.ReportProgress(int.Parse(Math.Ceiling((index + 1.0) / count * 100).ToString(CultureInfo.CurrentCulture)));
  470. }
  471. //保存路径不存在时,创建
  472. if (!Directory.Exists(Dir))
  473. {
  474. Directory.CreateDirectory(Dir);
  475. }
  476. //若文件不存在则创建
  477. var filePath = Dir + "errors.txt";
  478. var logFile = new StreamWriter(filePath, false);
  479. //写入
  480. logFile.Write(errLog);
  481. //关闭
  482. logFile.Close();
  483. logFile.Dispose();
  484. }
  485. /// <summary>
  486. /// 响应_buildWorker的ProgressChanged事件
  487. /// </summary>
  488. private void _buildWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
  489. {
  490. buildProgress.Value = e.ProgressPercentage;
  491. buildText.Text += _buildLog;
  492. }
  493. /// <summary>
  494. /// 响应_buildWorker的RunWorkerCompleted事件
  495. /// </summary>
  496. private void _buildWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
  497. {
  498. MessageBox.Show(e.Cancelled ? @"进程已被取消:" + buildProgress.Value + "%" : @"进程执行完成:" + buildProgress.Value + "%");
  499. buildProgress.Value = 0;
  500. }
  501. #endregion
  502. #region 内部方法
  503. /// <summary>
  504. /// 获取并绑定编译工具
  505. /// </summary>
  506. private void BindVersion()
  507. {
  508. //获取MSBuild
  509. var exeName = "MSBuild.exe";
  510. var msBuilds = Directory.GetFiles(@"C:\Program Files (x86)", exeName, SearchOption.AllDirectories);
  511. if (msBuilds.Length == 0)
  512. {
  513. MessageBox.Show(@"请先安装vs");
  514. return;
  515. }
  516. var listVersion = (from msBuild in msBuilds
  517. let split = msBuild.Split(new[] { "\\", "/" }, StringSplitOptions.RemoveEmptyEntries)
  518. where split.Length > 3
  519. let amd = split[split.Length - 2]
  520. let version = amd.Contains("64") ? "(x64)" : "(x86)"
  521. select new ItemModel(split[3] + version, msBuild))
  522. .ToList();
  523. versionCom.DisplayMember = "Key";
  524. versionCom.ValueMember = "Value";
  525. versionCom.DataSource = listVersion;
  526. }
  527. /// <summary>
  528. /// 绑定编译模式
  529. /// </summary>
  530. private void BindModel()
  531. {
  532. var dataSource = new List<ItemModel>
  533. {
  534. new ItemModel("1", "Debug"),
  535. new ItemModel("2", "Release")
  536. };
  537. //编译模式
  538. configureBox.DisplayMember = "Value";
  539. configureBox.ValueMember = "Key";
  540. configureBox.DataSource = dataSource;
  541. }
  542. /// <summary>
  543. /// 获取当前配置
  544. /// </summary>
  545. private ConfigureModel GetConfigModel()
  546. {
  547. var config = new ConfigureModel()
  548. {
  549. Url = projectFileText.Text,
  550. PathUrl = dllFileText.Text,
  551. OutputUrl = outputText.Text
  552. };
  553. if (versionCom.SelectedItem is ItemModel)
  554. {
  555. ItemModel versionItem = versionCom.SelectedItem as ItemModel;
  556. config.Version = versionItem.Key;
  557. config.VersionUlr = versionItem.Value;
  558. }
  559. if (configureBox.SelectedItem is ItemModel)
  560. {
  561. ItemModel modelItem = configureBox.SelectedItem as ItemModel;
  562. config.Model = modelItem.Value;
  563. config.ModelKey = modelItem.Key;
  564. }
  565. return config;
  566. }
  567. /// <summary>
  568. /// 删除文件夹下的所有文件
  569. /// </summary>
  570. private void DeleteDir(string srcPath)
  571. {
  572. try
  573. {
  574. DirectoryInfo dir = new DirectoryInfo(srcPath);
  575. FileSystemInfo[] fileInfo = dir.GetFileSystemInfos();
  576. foreach (FileSystemInfo file in fileInfo)
  577. {
  578. if (file is DirectoryInfo)
  579. {
  580. DirectoryInfo subDir = new DirectoryInfo(file.FullName);
  581. subDir.Delete(true);
  582. }
  583. else
  584. {
  585. File.Delete(file.FullName);
  586. }
  587. }
  588. }
  589. catch (Exception)
  590. {
  591. throw;
  592. }
  593. }
  594. #endregion
  595. }
  596. }