diff --git a/.github/workflows/test-neo-cli.expect b/.github/workflows/test-neo-cli.expect index dbf41526b..21ee4ceda 100755 --- a/.github/workflows/test-neo-cli.expect +++ b/.github/workflows/test-neo-cli.expect @@ -14,6 +14,10 @@ expect { timeout { exit 1 } } +sleep 5 + +spawn dotnet out/neo-cli.dll + # # Test 'create wallet' # diff --git a/Neo.ConsoleService/ConsoleServiceBase.cs b/Neo.ConsoleService/ConsoleServiceBase.cs index ae55b5fd7..91b02cfd8 100644 --- a/Neo.ConsoleService/ConsoleServiceBase.cs +++ b/Neo.ConsoleService/ConsoleServiceBase.cs @@ -291,7 +291,14 @@ public virtual void OnStart(string[] args) public virtual void OnStop() { - _shutdownAcknowledged.Signal(); + try + { + _shutdownAcknowledged.Signal(); + } + catch + { + // ignore + } } public string ReadUserInput(string prompt, bool password = false) diff --git a/neo-cli/CLI/MainService.Mode.cs b/neo-cli/CLI/MainService.Mode.cs new file mode 100644 index 000000000..291450e92 --- /dev/null +++ b/neo-cli/CLI/MainService.Mode.cs @@ -0,0 +1,209 @@ +// Copyright (C) 2016-2022 The Neo Project. +// +// The neo-cli is free software distributed under the MIT software +// license, see the accompanying file LICENSE in the main directory of +// the project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Akka.Util.Internal; +using Neo.ConsoleService; +namespace Neo.CLI; + +partial class MainService +{ + /// + /// Process "mode list" command. + /// + [ConsoleCommand("mode list", Category = "Mode Commands")] + private void OnListModes() + { + try + { + Directory.GetDirectories(ModePath).ForEach(p => ConsoleHelper.Info(p)); + } + catch (IOException) + { + } + } + + /// + /// Process "mode save" command + /// Mode name + /// + [ConsoleCommand("mode save", Category = "Mode Commands")] + private void OnSaveMode(string modeName) + { + // if no mode name assigned + if (modeName is null) + { + ConsoleHelper.Error("No mode name assigned."); + return; + } + modeName = modeName.ToLower(); + try + { + var targetMode = new DirectoryInfo($"{ModePath}/{modeName}"); + // Create the mode if it does not exist + if (!targetMode.Exists) Directory.CreateDirectory(targetMode.FullName); + // Get the config files of the node + MoveModeConfig(modeName.ToLower()); + var plugins = new DirectoryInfo(PluginPath); + // Cache directories before we start copying + var dirs = plugins.GetDirectories(); + // Create an empty .PLUGINS file + File.Create($"{ModePath}/{modeName}/.PLUGINS").Close(); + // Save the Plugin files + foreach (var plugin in dirs) + { + foreach (var file in plugin.GetFiles().Where(p => p.Extension == ".json")) + { + file.CopyTo($"{ModePath}/{modeName}/{plugin.Name}.json", true); + } + AddPluginToMode(plugin.Name, modeName); + } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + /// + /// Process "mode delete" command + /// Mode name + /// + [ConsoleCommand("mode delete", Category = "Mode Commands")] + private void OnDeleteMode(string modeName) + { + try + { + var dir = new DirectoryInfo($"{ModePath}/{modeName.ToLower()}"); + if (!dir.Exists) + return; + Directory.Delete(dir.FullName, true); + ConsoleHelper.Info("Mode ", modeName, " was deleted."); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + /// + /// Load the target mode, plugin should be according to the mode, + /// if the mode contains the plugin, install the plugin, otherwise delete the plugin + /// + /// name of the mode + /// if the mode is not found + private async Task LoadMode(string mode) + { + try + { + mode = mode.ToLower(); + var dir = new DirectoryInfo($"{ModePath}/{mode}"); + if (!dir.Exists) + throw new DirectoryNotFoundException($"Mode not found: {dir.FullName}"); + // Process the plugin + var modePlugins = File.ReadAllLines($"{ModePath}/{_currentMode}/.PLUGINS"); + // loop modePlugins + foreach (var pluginName in modePlugins) + { + // if the plugin is not installed, install it + if (!Directory.Exists($"{PluginPath}/{pluginName}/")) + { + await InstallPluginAsync(pluginName, overWrite: true, saveConfig: false); + _needRestart = true; + } + // if the mode has the plugin config, load the config from the mode + if (File.Exists($"{ModePath}/{mode}/{pluginName}.json")) + File.Copy($"{ModePath}/{mode}/{pluginName}.json", + $"{PluginPath}/{pluginName}/config.json", true); + } + // get the system config file from the mode + MoveModeConfig(mode, false); + + // Get existing plugins and delete them if they are not in the mode + new DirectoryInfo($"{PluginPath}/").GetDirectories().ForEach(p => + { + if (modePlugins.Any(k => string.Compare(Path.GetFileNameWithoutExtension(k), p.Name, StringComparison.OrdinalIgnoreCase) == 0) + || !File.Exists($"{PluginPath}/{p.Name}/config.json")) return; + try + { + ConsoleHelper.Info("Removing plugin ", p.Name); + Directory.Delete($"{PluginPath}/{p.Name}", true); + _needRestart = true; + } + catch + { + // ignored + } + }); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + /// save config.json and config.fs.json to the mode directory + /// name of the mode + /// + /// if the mode is not found + private static void MoveModeConfig(string mode, bool toMode = true) + { + var modeDir = new DirectoryInfo($"{ModePath}/{mode.ToLower()}"); + var configDir = new DirectoryInfo($"{StrExeFilePath}"); + if (!modeDir.Exists) + throw new DirectoryNotFoundException($"Mode not found: {modeDir.FullName}"); + try + { + if (toMode) + { + File.Copy(configDir.FullName + "/config.json", + modeDir.FullName + "/config.json", true); + File.Copy(configDir.FullName + "/config.fs.json", + modeDir.FullName + "/config.fs.json", true); + } + else + { + File.Copy(modeDir.FullName + "/config.json", + configDir.FullName + "/config.json", true); + File.Copy(modeDir.FullName + "/config.fs.json", + configDir.FullName + "/config.fs.json", true); + } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + // Add plugin to .PLUGINS file + private static void AddPluginToMode(string pluginName, string modeName) + { + var plugins = File.ReadAllLines($"{ModePath}/{modeName}/.PLUGINS"); + if (plugins.Contains(pluginName)) return; + var newPlugins = plugins.Append(pluginName).ToArray(); + File.WriteAllLines($"{ModePath}/{modeName}/.PLUGINS", newPlugins); + } + + // Remove plugin from .PLUGINS file + private static void RemovePluginFromMode(string pluginName, string modeName) + { + var plugins = File.ReadAllLines($"{ModePath}/{modeName}/.PLUGINS"); + // if (plugins.All(p => !string.Equals(p, pluginName, StringComparison.CurrentCultureIgnoreCase))) return; + var newPlugins = plugins.Where(p => !string.Equals(p, pluginName, StringComparison.CurrentCultureIgnoreCase)).ToArray(); + File.WriteAllLines($"{ModePath}/{modeName}/.PLUGINS", newPlugins); + } +} diff --git a/neo-cli/CLI/MainService.Plugins.cs b/neo-cli/CLI/MainService.Plugins.cs index 6373c9c4a..b06cb0877 100644 --- a/neo-cli/CLI/MainService.Plugins.cs +++ b/neo-cli/CLI/MainService.Plugins.cs @@ -1,4 +1,5 @@ // Copyright (C) 2016-2023 The Neo Project. + // The neo-cli is free software distributed under the MIT software // license, see the accompanying file LICENSE in the main directory of // the project or http://www.opensource.org/licenses/mit-license.php @@ -34,7 +35,7 @@ private async Task OnInstallCommandAsync(string pluginName) { if (PluginExists(pluginName)) { - ConsoleHelper.Warning($"Plugin already exist."); + ConsoleHelper.Warning("Plugin already exist."); return; } @@ -119,9 +120,11 @@ private async Task DownloadPluginAsync(string pluginName) /// Install plugin from stream /// /// name of the plugin + /// installed dependency /// Install by force for `update` + /// Need to save the config file to the mode private async Task InstallPluginAsync(string pluginName, HashSet installed = null, - bool overWrite = false) + bool overWrite = false, bool saveConfig = true) { installed ??= new HashSet(); if (!installed.Add(pluginName)) return; @@ -140,7 +143,40 @@ private async Task InstallPluginAsync(string pluginName, HashSet install await using Stream es = entry.Open(); await InstallDependenciesAsync(es, installed); } - zip.ExtractToDirectory("./", true); + if (!Directory.Exists($"{StrExeFilePath}/Plugins")) + { + Directory.CreateDirectory($"{StrExeFilePath}/Plugins"); + } + zip.ExtractToDirectory($"{StrExeFilePath}", true); + Console.WriteLine(); + + if (!saveConfig) return; + // Save the config.json to current mode + try + { + var pluginActualName = GetPluginActualName(pluginName); + if (File.Exists($"{ModePath}/{_currentMode}/{pluginActualName}.json")) + { + if (File.Exists($"{PluginPath}/{pluginActualName}/config.json")) + // plugin contains config.json && mode contains plugin.json + // replace the config.json with plugin.json from mode + File.Copy($"{ModePath}/{_currentMode}/{pluginActualName}.json", $"{PluginPath}/{pluginActualName}/config.json", true); + else + // plugin doesn't contain config.json && mode contains plugin.json + // delete the plugin.json from mode + File.Delete($"{ModePath}/{_currentMode}/{pluginActualName}.json"); + } + else if (File.Exists($"{PluginPath}/{pluginActualName}/config.json")) + // plugin contains config.json && mode doesn't contain plugin.json + // copy the config.json to mode + File.Copy($"{PluginPath}/{pluginActualName}/config.json", $"{ModePath}/{_currentMode}/{pluginActualName}.json", false); + AddPluginToMode(pluginActualName, _currentMode); + } + catch (Exception e) + { + // ignored + Console.WriteLine(e.Message); + } } /// @@ -173,7 +209,8 @@ private async Task InstallDependenciesAsync(Stream config, HashSet insta /// private static bool PluginExists(string pluginName) { - return Plugin.Plugins.Any(p => p.Name.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)); + return Plugin.Plugins.Any(p => p.Name.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)) || + new DirectoryInfo($"{StrExeFilePath}/Plugins").GetDirectories().Any(p => p.Name.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)); } /// @@ -188,12 +225,12 @@ private void OnUnInstallCommand(string pluginName) ConsoleHelper.Warning("Plugin not found"); return; } - + pluginName = GetPluginActualName(pluginName); foreach (var p in Plugin.Plugins) { try { - using var reader = File.OpenRead($"./Plugins/{p.Name}/config.json"); + using var reader = File.OpenRead($"{PluginPath}/{p.Name}/config.json"); if (new ConfigurationBuilder() .AddJsonStream(reader) .Build() @@ -214,7 +251,11 @@ private void OnUnInstallCommand(string pluginName) } try { - Directory.Delete($"Plugins/{pluginName}", true); + Directory.Delete($"{PluginPath}/{pluginName}", true); + var config = $"{ModePath}/{_currentMode}/{pluginName}.json"; + if (File.Exists(config)) + File.Delete(config); + RemovePluginFromMode(pluginName, _currentMode); } catch (IOException) { } ConsoleHelper.Info("Uninstall successful, please restart neo-cli."); @@ -240,5 +281,17 @@ private void OnPluginsCommand() ConsoleHelper.Warning("No loaded plugins"); } } + + private static string GetPluginActualName(string pluginName) + { + var pluginActualName = ""; + foreach (var plugin in new DirectoryInfo($"{PluginPath}").GetDirectories()) + { + if (!string.Equals(plugin.Name, pluginName, StringComparison.CurrentCultureIgnoreCase)) continue; + pluginActualName = plugin.Name; + break; + } + return pluginActualName; + } } } diff --git a/neo-cli/CLI/MainService.cs b/neo-cli/CLI/MainService.cs index 74c288757..a64dab410 100644 --- a/neo-cli/CLI/MainService.cs +++ b/neo-cli/CLI/MainService.cs @@ -2,9 +2,9 @@ // // The neo-cli is free software distributed under the MIT software // license, see the accompanying file LICENSE in the main directory of -// the project or http://www.opensource.org/licenses/mit-license.php +// the project or http://www.opensource.org/licenses/mit-license.php // for more details. -// +// // Redistribution and use in source and binary forms with or without // modifications are permitted. @@ -46,6 +46,13 @@ public partial class MainService : ConsoleServiceBase, IWalletProvider private Wallet _currentWallet; public LocalNode LocalNode; + private static string _currentMode = "mainnet"; + private bool _needRestart = false; + + static readonly string StrExeFilePath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + static readonly string ModePath = Path.Combine(StrExeFilePath, "Modes"); + static readonly string PluginPath = Path.Combine(StrExeFilePath, "Plugins"); + public Wallet CurrentWallet { @@ -372,6 +379,8 @@ public async void Start(string[] args) { if (NeoSystem != null) return; bool verifyImport = true; + // set mainnet as default mode + string mode = "mainnet"; for (int i = 0; i < args.Length; i++) switch (args[i]) { @@ -379,7 +388,36 @@ public async void Start(string[] args) case "--noverify": verifyImport = false; break; + case "--mode": + case "/mode": + { + if (i < args.Length - 1) + { + i++; + // Get all the modes + var modes = Directory.GetDirectories($"{ModePath}/"); + // Find the expected mode + if (modes.Any(p => string.Equals(new DirectoryInfo(p).Name, args[i], StringComparison.CurrentCultureIgnoreCase))) + { + mode = args[i].ToLower(); + _currentMode = mode; + } + else + { + throw new Exception($"Invalid Mode {args[i]}."); + } + } + } + break; } + // Load the mode (network) + await LoadMode(mode); + if (_needRestart) + { + ConsoleHelper.Warning("Please restart the node to apply the changes."); + OnStop(); + return; + } ProtocolSettings protocol = ProtocolSettings.Load("config.json"); @@ -391,7 +429,6 @@ public async void Start(string[] args) foreach (var plugin in Plugin.Plugins) { // Register plugins commands - RegisterCommand(plugin, plugin.Name); } diff --git a/neo-cli/Modes/mainnet/.PLUGINS b/neo-cli/Modes/mainnet/.PLUGINS new file mode 100644 index 000000000..3e16d9f30 --- /dev/null +++ b/neo-cli/Modes/mainnet/.PLUGINS @@ -0,0 +1 @@ +LevelDBStore diff --git a/neo-cli/config.fs.mainnet.json b/neo-cli/Modes/mainnet/config.fs.json similarity index 100% rename from neo-cli/config.fs.mainnet.json rename to neo-cli/Modes/mainnet/config.fs.json diff --git a/neo-cli/config.mainnet.json b/neo-cli/Modes/mainnet/config.json similarity index 100% rename from neo-cli/config.mainnet.json rename to neo-cli/Modes/mainnet/config.json diff --git a/neo-cli/Modes/privatenet/.PLUGINS b/neo-cli/Modes/privatenet/.PLUGINS new file mode 100644 index 000000000..61c2a05da --- /dev/null +++ b/neo-cli/Modes/privatenet/.PLUGINS @@ -0,0 +1,4 @@ +LevelDBStore +RpcServer +ApplicationLogs +DBFTPlugin diff --git a/neo-cli/Modes/privatenet/NbVj8GhwToNv4WF2gVaoco6hbkMQ8hrHWP.json b/neo-cli/Modes/privatenet/NbVj8GhwToNv4WF2gVaoco6hbkMQ8hrHWP.json new file mode 100644 index 000000000..823a076be --- /dev/null +++ b/neo-cli/Modes/privatenet/NbVj8GhwToNv4WF2gVaoco6hbkMQ8hrHWP.json @@ -0,0 +1 @@ +{"extra":{},"scrypt":{"r":8,"p":8,"n":16384,"size":64},"name":"NbVj8GhwToNv4WF2gVaoco6hbkMQ8hrHWP","accounts":[{"address":"NbVj8GhwToNv4WF2gVaoco6hbkMQ8hrHWP","extra":{},"lock":false,"contract":{"script":"DCED7s02MiDW5N55Or\/rrgl3l2hGjFmjtuATKp6jsiAQ99hBVuezJw==","deployed":false,"parameters":[{"name":"signature","type":"Signature"}]},"label":"NbVj8GhwToNv4WF2gVaoco6hbkMQ8hrHWP","key":"6PYTrLoLMT1Ms1H2msFTGUDRhHKXsZWA2Lj5rcF2GdPJju1Nuqy4npbef4","isDefault":true}],"version":"1.0"} diff --git a/neo-cli/Modes/privatenet/config.fs.json b/neo-cli/Modes/privatenet/config.fs.json new file mode 100644 index 000000000..dadf6f5bf --- /dev/null +++ b/neo-cli/Modes/privatenet/config.fs.json @@ -0,0 +1,53 @@ +{ + "ApplicationConfiguration": { + "Logger": { + "Path": "Logs", + "ConsoleOutput": false, + "Active": false + }, + "Storage": { + "Engine": "LevelDBStore", + "Path": "Data_LevelDB_{0}" + }, + "P2P": { + "Port": 40333, + "WsPort": 40334, + "MinDesiredConnections": 10, + "MaxConnections": 40, + "MaxConnectionsPerAddress": 3 + }, + "UnlockWallet": { + "Path": "", + "Password": "", + "IsActive": false + } + }, + "ProtocolConfiguration": { + "Network": 91414437, + "AddressVersion": 53, + "MillisecondsPerBlock": 15000, + "MaxTransactionsPerBlock": 512, + "MemoryPoolMaxTransactions": 50000, + "MaxTraceableBlocks": 2102400, + "InitialGasDistribution": 5200000000000000, + "ValidatorsCount": 7, + "StandbyCommittee": [ + "026fa34ec057d74c2fdf1a18e336d0bd597ea401a0b2ad57340d5c220d09f44086", + "039a9db2a30942b1843db673aeb0d4fd6433f74cec1d879de6343cb9fcf7628fa4", + "0366d255e7ce23ea6f7f1e4bedf5cbafe598705b47e6ec213ef13b2f0819e8ab33", + "023f9cb7bbe154d529d5c719fdc39feaa831a43ae03d2a4280575b60f52fa7bc52", + "039ba959e0ab6dc616df8b803692f1c30ba9071b76b05535eb994bf5bbc402ad5f", + "035a2a18cddafa25ad353dea5e6730a1b9fcb4b918c4a0303c4387bb9c3b816adf", + "031f4d9c66f2ec348832c48fd3a16dfaeb59e85f557ae1e07f6696d0375c64f97b" + ], + "SeedList": [ + "morph1.fs.neo.org:40333", + "morph2.fs.neo.org:40333", + "morph3.fs.neo.org:40333", + "morph4.fs.neo.org:40333", + "morph5.fs.neo.org:40333", + "morph6.fs.neo.org:40333", + "morph7.fs.neo.org:40333" + ] + } +} diff --git a/neo-cli/Modes/privatenet/config.json b/neo-cli/Modes/privatenet/config.json new file mode 100644 index 000000000..05656f76e --- /dev/null +++ b/neo-cli/Modes/privatenet/config.json @@ -0,0 +1,39 @@ +{ + "ApplicationConfiguration": { + "Logger": { + "Path": "Logs", + "ConsoleOutput": true, + "Active": true + }, + "Storage": { + "Engine": "LevelDBStore", + "Path": "Data_LevelDB_{0}" + }, + "P2P": { + "Port": 10333, + "WsPort": 10334, + "MinDesiredConnections": 10, + "MaxConnections": 40, + "MaxConnectionsPerAddress": 3 + }, + "UnlockWallet": { + "Path": "Modes/privatenet/NbVj8GhwToNv4WF2gVaoco6hbkMQ8hrHWP.json", + "Password": "12345678", + "IsActive": true + } + }, + "ProtocolConfiguration": { + "Network": 582277343, + "AddressVersion": 53, + "MillisecondsPerBlock": 15000, + "MaxTransactionsPerBlock": 512, + "MemoryPoolMaxTransactions": 50000, + "MaxTraceableBlocks": 2102400, + "InitialGasDistribution": 5200000000000000, + "ValidatorsCount": 1, + "StandbyCommittee": [ + "03eecd363220d6e4de793abfebae09779768468c59a3b6e0132a9ea3b22010f7d8" + ], + "SeedList": [] + } +} diff --git a/neo-cli/Modes/testnet/.PLUGINS b/neo-cli/Modes/testnet/.PLUGINS new file mode 100644 index 000000000..3e16d9f30 --- /dev/null +++ b/neo-cli/Modes/testnet/.PLUGINS @@ -0,0 +1 @@ +LevelDBStore diff --git a/neo-cli/config.fs.testnet.json b/neo-cli/Modes/testnet/config.fs.json similarity index 100% rename from neo-cli/config.fs.testnet.json rename to neo-cli/Modes/testnet/config.fs.json diff --git a/neo-cli/config.testnet.json b/neo-cli/Modes/testnet/config.json similarity index 100% rename from neo-cli/config.testnet.json rename to neo-cli/Modes/testnet/config.json diff --git a/neo-cli/config.fs.json b/neo-cli/config.fs.json new file mode 100644 index 000000000..94fb94a2a --- /dev/null +++ b/neo-cli/config.fs.json @@ -0,0 +1,53 @@ +{ + "ApplicationConfiguration": { + "Logger": { + "Path": "Logs", + "ConsoleOutput": false, + "Active": false + }, + "Storage": { + "Engine": "LevelDBStore", + "Path": "Data_LevelDB_{0}" + }, + "P2P": { + "Port": 40333, + "WsPort": 40334, + "MinDesiredConnections": 10, + "MaxConnections": 40, + "MaxConnectionsPerAddress": 3 + }, + "UnlockWallet": { + "Path": "", + "Password": "", + "IsActive": false + } + }, + "ProtocolConfiguration": { + "Network": 91414437, + "AddressVersion": 53, + "MillisecondsPerBlock": 15000, + "MaxTransactionsPerBlock": 512, + "MemoryPoolMaxTransactions": 50000, + "MaxTraceableBlocks": 2102400, + "InitialGasDistribution": 5200000000000000, + "ValidatorsCount": 7, + "StandbyCommittee": [ + "026fa34ec057d74c2fdf1a18e336d0bd597ea401a0b2ad57340d5c220d09f44086", + "039a9db2a30942b1843db673aeb0d4fd6433f74cec1d879de6343cb9fcf7628fa4", + "0366d255e7ce23ea6f7f1e4bedf5cbafe598705b47e6ec213ef13b2f0819e8ab33", + "023f9cb7bbe154d529d5c719fdc39feaa831a43ae03d2a4280575b60f52fa7bc52", + "039ba959e0ab6dc616df8b803692f1c30ba9071b76b05535eb994bf5bbc402ad5f", + "035a2a18cddafa25ad353dea5e6730a1b9fcb4b918c4a0303c4387bb9c3b816adf", + "031f4d9c66f2ec348832c48fd3a16dfaeb59e85f557ae1e07f6696d0375c64f97b" + ], + "SeedList": [ + "morph1.fs.neo.org:40333", + "morph2.fs.neo.org:40333", + "morph3.fs.neo.org:40333", + "morph4.fs.neo.org:40333", + "morph5.fs.neo.org:40333", + "morph6.fs.neo.org:40333", + "morph7.fs.neo.org:40333" + ] + } +} diff --git a/neo-cli/neo-cli.csproj b/neo-cli/neo-cli.csproj index 331b60d8b..d6c21172e 100644 --- a/neo-cli/neo-cli.csproj +++ b/neo-cli/neo-cli.csproj @@ -17,14 +17,15 @@ - + + PreserveNewest + PreserveNewest + Modes\%(RecursiveDir)\%(Filename)%(Extension) + - - PreserveNewest - PreserveNewest - + @@ -34,5 +35,4 @@ -