From 7aa70a2eeb5ab72e1e96f33535a75d7642ac10eb Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 21 Jun 2025 18:33:38 +0900 Subject: [PATCH] Try to apply ESENT database #277 --- .../WelsonJS.Launcher/InstancesForm.cs | 3 + .../WelsonJS.Launcher/MainForm.cs | 27 +- .../WelsonJS.Launcher/MetadataStore.cs | 380 ++++++++++++++++++ WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs | 26 +- .../WelsonJS.Launcher/Storage/Column.cs | 66 +++ .../WelsonJS.Launcher/Storage/Schema.cs | 42 ++ .../WelsonJS.Launcher.csproj | 25 ++ .../WelsonJS.Launcher/packages.config | 4 + 8 files changed, 564 insertions(+), 9 deletions(-) create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/MetadataStore.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/Storage/Column.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/Storage/Schema.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/packages.config diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/InstancesForm.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/InstancesForm.cs index fcbcbb0..c9591f8 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/InstancesForm.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/InstancesForm.cs @@ -15,17 +15,20 @@ namespace WelsonJS.Launcher private string entryFileName; private string scriptName; private const string timestampFormat = "yyyy-MM-dd HH:mm:ss"; + private static MetadataStore metadataStore; public InstancesForm() { InitializeComponent(); + // set the entry file name to run the instance entryFileName = "bootstrap.bat"; } private void InstancesForm_Load(object sender, EventArgs e) { lvInstances.Items.Clear(); + LoadInstances(Program.GetAppDataPath()); LoadInstances(Path.GetTempPath()); } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs index 98b2be8..e8e1e13 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs @@ -4,6 +4,7 @@ // https://github.com/gnh1201/welsonjs // using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; @@ -158,6 +159,7 @@ namespace WelsonJS.Launcher private void RecordFirstDeployTime(string directory) { + /* try { string filePath = Path.Combine(directory, ".welsonjs_first_deploy_time"); @@ -169,6 +171,21 @@ namespace WelsonJS.Launcher { throw new Exception($"Failed to record first deploy time: {ex.Message}"); } + */ + + try + { + object key; + Program._MetadataStore.Insert(new Dictionary + { + ["InstanceId"] = "abc123", + ["FirstDeployTime"] = "2025-06-19 10:00:00" + }, out key); + } + catch (Exception ex) + { + Trace.TraceWarning(ex.Message); + } } private bool IsInAdministrator() @@ -242,26 +259,26 @@ namespace WelsonJS.Launcher { Program.StartResourceServer(); - if (!Program.resourceServer.IsRunning()) + if (!Program._ResourceServer.IsRunning()) { - Program.resourceServer.Start(); + Program._ResourceServer.Start(); ((ToolStripMenuItem)sender).Text = "Open the code editor..."; } else { - Program.OpenWebBrowser(Program.resourceServer.GetPrefix()); + Program.OpenWebBrowser(Program._ResourceServer.GetPrefix()); } } private void openCodeEditorToolStripMenuItem_Click(object sender, EventArgs e) { - if (Program.resourceServer == null) + if (Program._ResourceServer == null) { MessageBox.Show("A resource server is not running."); } else { - Program.OpenWebBrowser(Program.resourceServer.GetPrefix()); + Program.OpenWebBrowser(Program._ResourceServer.GetPrefix()); } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/MetadataStore.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/MetadataStore.cs new file mode 100644 index 0000000..133ffe6 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MetadataStore.cs @@ -0,0 +1,380 @@ +// MetadataStore.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.Isam.Esent.Interop; +using WelsonJS.Launcher.Storage; + +namespace WelsonJS.Launcher +{ + public class MetadataStore : IDisposable + { + private static readonly object _lock = new object(); + private static bool _initialized = false; + private static Instance _instance; + private static string _workingDirectory; + private static string _filePath; + + private readonly Session _session; + private readonly JET_DBID _dbid; + private readonly Schema _schema; + private readonly Column _primaryKey; + + private Dictionary _columnIds; + + public MetadataStore(Schema schema) + { + _primaryKey = schema.PrimaryKey; + + if (schema == null) + throw new ArgumentNullException(nameof(schema)); + + if (_primaryKey == null) + throw new ArgumentNullException(); + + if (!schema.Columns.Exists(c => c == _primaryKey)) + throw new ArgumentException($"Primary key '{_primaryKey.Name}' is not in schema."); + + _workingDirectory = Program.GetAppDataPath(); + _schema = schema; + _columnIds = new Dictionary(StringComparer.OrdinalIgnoreCase); + + InitializeInstance(); + + _session = new Session(_instance); + + if (!File.Exists(_filePath)) + { + Api.JetCreateDatabase(_session, _filePath, null, out _dbid, CreateDatabaseGrbit.None); + CreateTable(_schema); + } + else + { + Api.JetAttachDatabase(_session, _filePath, AttachDatabaseGrbit.None); + Api.JetOpenDatabase(_session, _filePath, null, out _dbid, OpenDatabaseGrbit.None); + } + + CacheColumns(); + } + + private static void InitializeInstance() + { + if (_initialized) return; + + lock (_lock) + { + if (_initialized) return; + + // set the file path + _filePath = Path.Combine(_workingDirectory, "metadata.edb"); + + // config the instance + _instance = new Instance("WelsonJS.Launcher.MetadataStore"); + _instance.Parameters.SystemDirectory = _workingDirectory; + _instance.Parameters.LogFileDirectory = _workingDirectory; + _instance.Parameters.TempDirectory = _workingDirectory; + + // initialize the instance + _instance.Init(); + _initialized = true; + } + } + + private void CreateTable(Schema schema) + { + Api.JetBeginTransaction(_session); + JET_TABLEID tableid; + Api.JetCreateTable(_session, _dbid, schema.TableName, 0, 100, out tableid); + + foreach (var col in schema.Columns) + { + var coldef = new JET_COLUMNDEF + { + coltyp = col.Type, + cbMax = col.MaxSize, + cp = col.CodePage + }; + Api.JetAddColumn(_session, tableid, col.Name, coldef, null, 0, out _); + } + + Api.JetCloseTable(_session, tableid); + Api.JetCommitTransaction(_session, CommitTransactionGrbit.None); + } + + private void CacheColumns() + { + using (var table = new Table(_session, _dbid, _schema.TableName, OpenTableGrbit.ReadOnly)) + { + foreach (var col in _schema.Columns) + { + try + { + JET_COLUMNID colid = Api.GetTableColumnid(_session, table, col.Name); + _columnIds[col.Name] = colid; + } + catch (EsentColumnNotFoundException) + { + Trace.TraceWarning($"Column '{col.Name}' not found."); + } + } + } + } + + public bool Insert(Dictionary values, out object key) + { + return TrySaveRecord(values, JET_prep.Insert, expectSeek: false, out key); + } + + public bool Update(Dictionary values) + { + return TrySaveRecord(values, JET_prep.Replace, expectSeek: true, out _); + } + + private bool TrySaveRecord( + Dictionary values, + JET_prep prepType, + bool expectSeek, + out object primaryKeyValue) + { + primaryKeyValue = null; + + if (!TryGetPrimaryKeyValue(values, out var keyValue)) + return false; + + var keyType = _primaryKey.Type; + + using (var table = new Table(_session, _dbid, _schema.TableName, OpenTableGrbit.Updatable)) + { + try + { + Api.JetBeginTransaction(_session); + + MakeKeyByType(keyValue, keyType, _session, table); + bool found = Api.TrySeek(_session, table, SeekGrbit.SeekEQ); + + if (expectSeek != found) + { + Trace.TraceWarning($"[ESENT] Operation skipped. Seek result = {found}, expected = {expectSeek}"); + Api.JetRollback(_session, RollbackTransactionGrbit.None); + return false; + } + + Api.JetPrepareUpdate(_session, table, prepType); + SetAllColumns(values, table); + + Api.JetUpdate(_session, table); + Api.JetCommitTransaction(_session, CommitTransactionGrbit.None); + + if (prepType == JET_prep.Insert) + primaryKeyValue = keyValue; + + return true; + } + catch (Exception ex) + { + Trace.TraceError($"[ESENT] Operation failed: {ex.Message}"); + Api.JetRollback(_session, RollbackTransactionGrbit.None); + return false; + } + } + } + + public Dictionary FindById(object keyValue) + { + var result = new Dictionary(); + var keyType = _primaryKey.Type; + + using (var table = new Table(_session, _dbid, _schema.TableName, OpenTableGrbit.ReadOnly)) + { + MakeKeyByType(keyValue, keyType, _session, table); + if (!Api.TrySeek(_session, table, SeekGrbit.SeekEQ)) + return null; + + foreach (var col in _schema.Columns) + { + if (!_columnIds.TryGetValue(col.Name, out var colid)) + continue; + + var value = RetrieveColumnByType(_session, table, colid, col.Type); + result[col.Name] = value; + } + } + + return result; + } + + public List> FindAll() + { + var results = new List>(); + + using (var table = new Table(_session, _dbid, _schema.TableName, OpenTableGrbit.ReadOnly)) + { + if (!Api.TryMoveFirst(_session, table)) + return results; + + do + { + var row = new Dictionary(); + foreach (var col in _schema.Columns) + { + if (!_columnIds.TryGetValue(col.Name, out var colid)) + continue; + + var value = RetrieveColumnByType(_session, table, colid, col.Type); + row[col.Name] = value; + } + results.Add(row); + } + while (Api.TryMoveNext(_session, table)); + } + + return results; + } + + public bool DeleteById(object keyValue) + { + var keyType = _primaryKey.Type; + + using (var table = new Table(_session, _dbid, _schema.TableName, OpenTableGrbit.Updatable)) + { + MakeKeyByType(keyValue, keyType, _session, table); + if (!Api.TrySeek(_session, table, SeekGrbit.SeekEQ)) + return false; + + Api.JetDelete(_session, table); + return true; + } + } + + private object RetrieveColumnByType(Session session, Table table, JET_COLUMNID columnId, JET_coltyp type) + { + switch (type) + { + case JET_coltyp.Text: + return Api.RetrieveColumnAsString(session, table, columnId, Encoding.Unicode); + case JET_coltyp.Long: + return Api.RetrieveColumnAsInt32(session, table, columnId); + case JET_coltyp.IEEEDouble: + return Api.RetrieveColumnAsDouble(session, table, columnId); + case JET_coltyp.DateTime: + return Api.RetrieveColumnAsDateTime(session, table, columnId); + case JET_coltyp.Binary: + case JET_coltyp.LongBinary: + return Api.RetrieveColumn(session, table, columnId); + default: + Trace.TraceWarning($"[ESENT] Unsupported RetrieveColumn type: {type}"); + return null; + } + } + + private bool TryGetPrimaryKeyValue(Dictionary values, out object keyValue) + { + keyValue = null; + + if (!values.TryGetValue(_primaryKey.Name, out keyValue)) + { + Trace.TraceWarning($"[ESENT] Missing primary key '{_primaryKey.Name}'."); + return false; + } + + if (keyValue == null) + { + Trace.TraceWarning("[ESENT] Primary key value cannot be null."); + return false; + } + + return true; + } + + private JET_coltyp GetColumnType(string columnName) + { + var column = _schema.Columns.FirstOrDefault(c => c.Name == columnName); + if (column == null) + throw new ArgumentException($"Column '{columnName}' not found in schema."); + + return column.Type; + } + + private void SetAllColumns(Dictionary values, Table table) + { + foreach (var kv in values) + { + if (!_columnIds.TryGetValue(kv.Key, out var colid)) + { + Trace.TraceWarning($"[ESENT] Column '{kv.Key}' not found in cache."); + continue; + } + + var colType = GetColumnType(kv.Key); + SetColumnByType(_session, table, colid, kv.Value, colType); + } + } + + private void SetColumnByType(Session session, Table table, JET_COLUMNID columnId, object value, JET_coltyp type) + { + if (value == null) + return; + + switch (type) + { + case JET_coltyp.Text: + Api.SetColumn(session, table, columnId, value.ToString(), Encoding.Unicode); + break; + case JET_coltyp.Long: + Api.SetColumn(session, table, columnId, Convert.ToInt32(value)); + break; + case JET_coltyp.IEEEDouble: + Api.SetColumn(session, table, columnId, Convert.ToDouble(value)); + break; + case JET_coltyp.DateTime: + Api.SetColumn(session, table, columnId, Convert.ToDateTime(value)); + break; + case JET_coltyp.Binary: + case JET_coltyp.LongBinary: + Api.SetColumn(session, table, columnId, (byte[])value); + break; + default: + Trace.TraceWarning($"[ESENT] Unsupported SetColumn type: {type}"); + break; + } + } + + private void MakeKeyByType(object value, JET_coltyp type, Session session, Table table) + { + switch (type) + { + case JET_coltyp.Text: + Api.MakeKey(session, table, value.ToString(), Encoding.Unicode, MakeKeyGrbit.NewKey); + break; + case JET_coltyp.Long: + Api.MakeKey(session, table, Convert.ToInt32(value), MakeKeyGrbit.NewKey); + break; + case JET_coltyp.IEEEDouble: + Api.MakeKey(session, table, Convert.ToDouble(value), MakeKeyGrbit.NewKey); + break; + case JET_coltyp.DateTime: + Api.MakeKey(session, table, Convert.ToDateTime(value), MakeKeyGrbit.NewKey); + break; + case JET_coltyp.Binary: + case JET_coltyp.LongBinary: + Api.MakeKey(session, table, (byte[])value, MakeKeyGrbit.NewKey); + break; + default: + Trace.TraceWarning($"[ESENT] Unsupported MakeKey type: {type}"); + break; + } + } + + public void Dispose() + { + _session?.Dispose(); + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index 80de408..c4d8f84 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -10,28 +10,46 @@ using System.Linq; using System.Threading; using System.Windows.Forms; using System.Configuration; +using System.Collections.Generic; +using WelsonJS.Launcher.Storage; namespace WelsonJS.Launcher { internal static class Program { static Mutex mutex; - public static ResourceServer resourceServer; + public static ResourceServer _ResourceServer; + public static MetadataStore _MetadataStore; [STAThread] static void Main() { - mutex = new Mutex(true, "WelsonJS.Launcher.Mutex", out bool isMutexNotExists); + // create the mutex + mutex = new Mutex(true, "WelsonJS.Launcher", out bool isMutexNotExists); if (!isMutexNotExists) { MessageBox.Show("WelsonJS Launcher already running."); return; } + // connect the database to manage an instances + Schema schema = new Schema("Instances", new List + { + new Column("InstanceId", typeof(string), 255), + new Column("FirstDeployTime", typeof(DateTime), 1) + }); + schema.SetPrimaryKey("InstanceId"); + _MetadataStore = new MetadataStore(schema); + + // draw the main form Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); + // close the database + _MetadataStore.Dispose(); + + // destory the mutex mutex.ReleaseMutex(); mutex.Dispose(); } @@ -147,9 +165,9 @@ namespace WelsonJS.Launcher { lock(typeof(Program)) { - if (resourceServer == null) + if (_ResourceServer == null) { - resourceServer = new ResourceServer(GetAppConfig("ResourceServerPrefix"), "editor.html"); + _ResourceServer = new ResourceServer(GetAppConfig("ResourceServerPrefix"), "editor.html"); } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Storage/Column.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Storage/Column.cs new file mode 100644 index 0000000..50d0571 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Storage/Column.cs @@ -0,0 +1,66 @@ +// Column.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Text; +using Microsoft.Isam.Esent.Interop; + +namespace WelsonJS.Launcher.Storage +{ + public class Column + { + public string Name { get; set; } + public JET_coltyp Type { get; set; } + public int MaxSize { get; set; } + public JET_CP CodePage { get; set; } + public bool IsPrimaryKey { get; set; } = false; + + public Column(string name, JET_coltyp type, int maxSize = 0, JET_CP codePage = JET_CP.None) + { + Name = name; + Type = type; + MaxSize = maxSize; + CodePage = codePage == JET_CP.None ? + JET_CP.Unicode : codePage; + } + + public Column(string name, Type dotNetType, int maxSize = 0, Encoding encoding = null) + { + Name = name; + Type = GetJetColtypFromType(dotNetType); + MaxSize = maxSize; + CodePage = GetJetCpFromEncoding(encoding ?? Encoding.Unicode); + } + + private static JET_coltyp GetJetColtypFromType(Type type) + { + if (type == typeof(string)) return JET_coltyp.Text; + if (type == typeof(int)) return JET_coltyp.Long; + if (type == typeof(long)) return JET_coltyp.Currency; + if (type == typeof(bool)) return JET_coltyp.Bit; + if (type == typeof(float)) return JET_coltyp.IEEESingle; + if (type == typeof(double)) return JET_coltyp.IEEEDouble; + if (type == typeof(DateTime)) return JET_coltyp.DateTime; + if (type == typeof(byte[])) return JET_coltyp.LongBinary; + + throw new NotSupportedException($"Unsupported .NET type: {type.FullName}"); + } + + private static JET_CP GetJetCpFromEncoding(Encoding encoding) + { + if (encoding == Encoding.Unicode) return JET_CP.Unicode; + if (encoding == Encoding.ASCII) return JET_CP.ASCII; + if (encoding.CodePage == 1252) return (JET_CP)1252; // Windows-1252 / Latin1 + if (encoding.CodePage == 51949) return (JET_CP)51949; // EUC-KR + if (encoding.CodePage == 949) return (JET_CP)949; // UHC (Windows Korean) + if (encoding.CodePage == 932) return (JET_CP)932; // Shift-JIS (Japanese) + if (encoding.CodePage == 936) return (JET_CP)936; // GB2312 (Simplified Chinese) + if (encoding.CodePage == 65001) return (JET_CP)65001; // UTF-8 + if (encoding.CodePage == 28591) return (JET_CP)28591; // ISO-8859-1 + + throw new NotSupportedException($"Unsupported encoding: {encoding.WebName} (code page {encoding.CodePage})"); + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Storage/Schema.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Storage/Schema.cs new file mode 100644 index 0000000..7b9bca3 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Storage/Schema.cs @@ -0,0 +1,42 @@ +// TableSchema.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Collections.Generic; + +namespace WelsonJS.Launcher.Storage +{ + public class Schema + { + public string TableName { get; set; } + public List Columns { get; set; } + public Column PrimaryKey + { + get + { + return Columns.Find(c => c.IsPrimaryKey) ?? null; + } + } + + public Schema(string tableName, List columns) + { + TableName = tableName; + Columns = columns ?? new List(); + } + + public void SetPrimaryKey(string columnName) + { + Column column = Columns.Find(c => c.Name.Equals(columnName, StringComparison.OrdinalIgnoreCase)); + if (column != null) + { + column.IsPrimaryKey = true; + } + else + { + throw new ArgumentException($"Column '{columnName}' does not exist in schema '{TableName}'."); + } + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index 0c27148..e20eb43 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -55,7 +55,28 @@ favicon.ico + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + 7.3 + prompt + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + 7.3 + prompt + + + ..\packages\ManagedEsent.1.9.4\lib\net40\Esent.Interop.dll + @@ -103,6 +124,9 @@ GlobalSettingsForm.cs + + + EnvForm.cs @@ -121,6 +145,7 @@ GlobalSettingsForm.cs + SettingsSingleFileGenerator Settings.Designer.cs diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/packages.config b/WelsonJS.Toolkit/WelsonJS.Launcher/packages.config new file mode 100644 index 0000000..cbda708 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file