Import/export monerod configuration file

This commit is contained in:
everoddandeven 2024-10-31 16:47:48 +01:00
parent dc8b35e250
commit d0d374f86c
9 changed files with 420 additions and 21 deletions

View file

@ -83,7 +83,7 @@ https://github.com/user-attachments/assets/c4a50d2f-5bbb-48ac-9425-30ecc20ada7c
- [X] Linux
- [X] Windows
- [X] MacOS
- [ ] Import/export `monerod.conf` node configuration
- [X] Import/export `monerod.conf` node configuration
- [X] Synchronization in a specific time slot
- [ ] Installers
- [X] Linux

View file

@ -759,6 +759,44 @@ try {
}
});
ipcMain.handle('read-file', (event: IpcMainInvokeEvent, filePath: string) => {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err != null) {
win?.webContents.send('on-read-file-error', `${err}`);
return;
}
win?.webContents.send('on-read-file', data);
});
});
ipcMain.handle('save-file', async (event: IpcMainInvokeEvent, defaultPath: string, content: string) => {
if (!win) {
return;
}
const result = await dialog.showSaveDialog(win, {
title: 'Save File',
defaultPath: defaultPath,
properties: [
'showOverwriteConfirmation'
]
});
if (result.canceled) {
win.webContents.send('on-save-file', '');
return;
}
try {
fs.writeFileSync(result.filePath, content);
win.webContents.send('on-save-file', result.filePath);
}
catch(error: any) {
win.webContents.send('on-save-file-error', `${error}`);
}
});
ipcMain.handle('select-file', async (event: IpcMainInvokeEvent, extensions?: string[]) => {
if (!win)
{
@ -794,6 +832,10 @@ try {
win.webContents.send('selected-folder', path ? `${path}` : '');
});
ipcMain.handle('get-path', (event: IpcMainInvokeEvent, path: 'home' | 'appData' | 'userData' | 'sessionData' | 'temp' | 'exe' | 'module' | 'desktop' | 'documents' | 'downloads' | 'music' | 'pictures' | 'videos' | 'recent' | 'logs' | 'crashDumps') => {
win?.webContents.send('on-get-path', app.getPath(path));
});
ipcMain.handle('is-wifi-connected', async (event: IpcMainInvokeEvent) => {
isWifiConnected();
});

View file

@ -69,9 +69,35 @@ contextBridge.exposeInMainWorld('electronAPI', {
onSelectedFolder: (callback) => {
ipcRenderer.on('selected-folder', callback);
},
readFile: (filePath) => {
ipcRenderer.invoke('read-file', filePath);
},
onReadFile: (callback) => {
ipcRenderer.on('on-read-file', callback);
},
onReadFileError: (callback) => {
ipcRenderer.on('on-read-file-error', callback);
},
unregisterOnReadFile: () => {
ipcRenderer.removeAllListeners('on-read-file');
ipcRenderer.removeAllListeners('on-read-file-error');
},
unregisterOnSelectedFolder: () => {
ipcRenderer.removeAllListeners('selected-folder');
},
saveFile: (defaultPath, content) => {
ipcRenderer.invoke('save-file', defaultPath, content);
},
onSaveFileError: (callback) => {
ipcRenderer.on('on-save-file-error', callback);
},
onSaveFile: (callback) => {
ipcRenderer.on('on-save-file', callback);
},
unregisterOnSaveFile: () => {
ipcRenderer.removeAllListeners('on-save-file-error');
ipcRenderer.removeAllListeners('on-save-file');
},
selectFile: (extensions = undefined) => {
ipcRenderer.invoke('select-file', extensions);
},
@ -90,6 +116,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
unregisterOnIsWifiConnectedResponse: () => {
ipcRenderer.removeAllListeners('is-wifi-connected-result');
},
getPath: (path) => {
ipcRenderer.invoke('get-path', path);
},
onGetPath: (callback) => {
ipcRenderer.on('on-get-path', callback);
},
unregisterOnGetPath: () => {
ipcRenderer.removeAllListeners('on-get-path');
},
getOsType: () => {
ipcRenderer.invoke('get-os-type');
},

View file

@ -227,6 +227,42 @@ export class ElectronService {
return await selectPromise;
}
public async readFile(filePath: string): Promise<string> {
const promise = new Promise<string>((resolve, reject) => {
window.electronAPI.onReadFileError((event: any, error: string) => {
window.electronAPI.unregisterOnReadFile();
reject(error);
});
window.electronAPI.onReadFile((event: any, data: string) => {
window.electronAPI.unregisterOnReadFile();
resolve(data);
});
});
window.electronAPI.readFile(filePath);
return await promise;
}
public async saveFile(defaultPath: string, content: string): Promise<string> {
const promise = new Promise<string>((resolve, reject) => {
window.electronAPI.onSaveFileError((event: any, error: string) => {
window.electronAPI.unregisterOnSaveFile();
reject(error);
});
window.electronAPI.onSaveFile((event: any, filePath: string) => {
window.electronAPI.unregisterOnSaveFile();
resolve(filePath);
})
});
window.electronAPI.saveFile(defaultPath, content);
return await promise;
}
public async selectFolder(): Promise<string> {
const selectPromise = new Promise<string>((resolve) => {
window.electronAPI.onSelectedFolder((event: any, folder: string) => {
@ -240,6 +276,18 @@ export class ElectronService {
return await selectPromise;
}
public async getPath(path: 'home' | 'appData' | 'userData' | 'sessionData' | 'temp' | 'exe' | 'module' | 'desktop' | 'documents' | 'downloads' | 'music' | 'pictures' | 'videos' | 'recent' | 'logs' | 'crashDumps'): Promise<string> {
const promise = new Promise<string>((resolve) => {
window.electronAPI.onGetPath((event: any, result: string) => {
window.electronAPI.unregisterOnGetPath();
resolve(result);
})
});
window.electronAPI.getPath(path);
return await promise;
}
public async getOsType(): Promise<{ platform: string, arch: string }> {
const promise = new Promise<{ platform: string, arch: string }>((resolve) => {

View file

@ -21,14 +21,55 @@
</div>
</div>
<div *ngIf="savingChangesSuccess" class="alert alert-success d-flex align-items-center justify-content-center text-center" role="alert">
<div *ngIf="successMessage !== ''" class="alert alert-success d-flex align-items-center justify-content-center text-center" role="alert">
<h4><i class="bi bi-check-circle m-2"></i></h4>&nbsp;&nbsp;
<div>
Successfully saved settings
{{ successMessage }}
</div>
</div>
<div class="tab-pane fade show active" id="pills-general" role="tabpanel" aria-labelledby="pills-general-tab" tabindex="0">
<div class="row g-5 p-2">
<div class="col-md-7 col-lg-12">
<h4 class="mb-3">Monerod</h4>
<div class="col-md-12">
<label for="general-monerod-path-control" class="form-label">Monerod path</label>
<div class="input-group mb-3">
<input id="general-monerod-path-control" type="text" class="form-control form-control-sm" placeholder="" aria-label="Monerod path" aria-describedby="basic-addon2" [value]="currentSettings.monerodPath" readonly>
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="chooseMonerodFile()">Choose file</button></span>
</div>
<small class="text-body-secondary">Path to monerod executable</small>
</div>
<hr class="my-4">
<h4 class="mb-3">Config file</h4>
<div class="row g-3">
<div class="col-md-6">
<label for="import-config-file" class="form-label">Import config file</label>
<div class="input-group mb-3">
<button type="button" class="btn btn-secondary btn-sm" (click)="importMonerodConfigFile()">Import</button>
</div>
<small class="text-body-secondary">Import custom monerod config file</small>
</div>
<div class="col-md-6">
<label for="export-config-file" class="form-label">Export config file</label>
<div class="input-group mb-3">
<button type="button" class="btn btn-secondary btn-sm" (click)="exportMonerodConfigFile()">Export</button>
</div>
<small class="text-body-secondary">Export current saved settings to config file</small>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-node" role="tabpanel" aria-labelledby="pills-node-tab" tabindex="0">
<div class="row g-5 p-2">
<div class="col-md-7 col-lg-12">
<h4 class="mb-3">Node</h4>
@ -67,15 +108,6 @@
<small class="text-body-secondary">Run in a regression testing mode</small>
</div>
<div class="col-md-12">
<label for="general-monerod-path-control" class="form-label">Monerod path</label>
<div class="input-group mb-3">
<input id="general-monerod-path-control" type="text" class="form-control form-control-sm" placeholder="" aria-label="Monerod path" aria-describedby="basic-addon2" [value]="currentSettings.monerodPath" readonly>
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="chooseMonerodFile()">Choose file</button></span>
</div>
<small class="text-body-secondary">Path to monerod executable</small>
</div>
<div class="col-md-12">
<label for="data-dir" class="form-label">Data dir</label>
<div class="input-group mb-3">

View file

@ -25,11 +25,14 @@ export class SettingsComponent {
public networkType: 'mainnet' | 'testnet' | 'stagenet' = 'mainnet';
public successMessage: string = '';
constructor(private daemonService: DaemonService, private electronService: ElectronService, private ngZone: NgZone) {
this.loading = true;
this.navbarLinks = [
new NavbarLink('pills-general-tab', '#pills-general', 'pills-general', true, 'General', false),
new NavbarLink('pills-node-tab', '#pills-node', 'pills-node', false, 'Node', false),
new NavbarLink('pills-rpc-tab', '#pills-rpc', 'pills-rpc', false, 'RPC', false),
new NavbarLink('pills-p2p-tab', '#pills-p2p', 'pills-p2p', false, 'P2P', false),
new NavbarLink('pills-blockchain-tab', '#pills-blockchain', 'pills-blockchain', false, 'Blockchain', false),
@ -189,10 +192,12 @@ export class SettingsComponent {
}
this.savingChangesError = ``;
this.successMessage = 'Successfully saved settings';
this.savingChangesSuccess = true;
}
catch(error: any) {
console.error(error);
this.successMessage = '';
this.savingChangesError = `${error}`;
this.savingChangesSuccess = false;
}
@ -216,6 +221,68 @@ export class SettingsComponent {
throw new Error("Could not get monerod mime type");
}
public async importMonerodConfigFile(): Promise<void> {
try {
try {
const filePath = await this.electronService.selectFile();
if (filePath == '') {
return;
}
const content = await this.electronService.readFile(filePath);
const settings = DaemonSettings.parseConfig(content);
settings.monerodPath = this.originalSettings.monerodPath;
settings.syncOnWifi = this.originalSettings.syncOnWifi;
settings.syncPeriodEnabled = this.originalSettings.syncPeriodEnabled;
settings.syncPeriodFrom = this.originalSettings.syncPeriodFrom;
settings.syncPeriodTo = this.originalSettings.syncPeriodTo;
settings.startAtLogin = this.originalSettings.startAtLogin;
settings.startAtLoginMinimized = this.originalSettings.startAtLoginMinimized;
settings.upgradeAutomatically = this.originalSettings.upgradeAutomatically;
settings.downloadUpgradePath = this.originalSettings.downloadUpgradePath;
this.currentSettings = settings;
await this.OnSave();
this.successMessage = 'Succesfully imported settings';
}
catch(error: any) {
console.error(error);
this.successMessage = '';
throw new Error("Could not parse monerod config file");
}
}
catch(error: any) {
console.error(error);
this.successMessage = '';
this.savingChangesError = `${error}`;
}
}
public async exportMonerodConfigFile(): Promise<void> {
try {
const config = this.originalSettings.toConfig();
const homePath = await this.electronService.getPath('home');
const resultPath = await this.electronService.saveFile(`${homePath}/monerod.conf`, config);
if (resultPath == '') {
return;
}
this.successMessage = 'Successfully exported config file to ' + resultPath;
this.savingChangesError = '';
}
catch(error: any) {
console.error(error);
this.successMessage = '';
this.savingChangesError = `${error}`;
}
}
public async chooseMonerodFile(): Promise<void> {
const spec = await this.getMonerodFileSpec();
const file = await this.electronService.selectFile(spec.extensions);

View file

@ -61,8 +61,8 @@ export class VersionComponent implements AfterViewInit {
private createCards(): SimpleBootstrapCard[] {
return [
new SimpleBootstrapCard('GUI Version', this.daemonService.getGuiVersion()),
new SimpleBootstrapCard('Current Monerod version', this.currentVersion ? this.currentVersion.fullname : '', this.currentVersion == null),
new SimpleBootstrapCard('Latest Monerod version', this.latestVersion ? this.latestVersion.fullname : '', this.latestVersion == null)
new SimpleBootstrapCard('Current Monerod version', this.currentVersion ? this.currentVersion.fullname : 'Not found', this.loading),
new SimpleBootstrapCard('Latest Monerod version', this.latestVersion ? this.latestVersion.fullname : 'Error', this.loading)
];
}
@ -87,17 +87,33 @@ export class VersionComponent implements AfterViewInit {
public loading: boolean = true;
private async refreshCurrentVersion(): Promise<void> {
try {
this.currentVersion = await this.daemonService.getVersion(true);
}
catch(error: any) {
console.error(error);
this.currentVersion = undefined;
}
}
private async refreshLatestVersion(): Promise<void> {
try {
this.latestVersion = await this.daemonService.getLatestVersion();
}
catch(error: any) {
console.error(error);
this.latestVersion = undefined;
}
}
public async load(): Promise<void> {
this.loading = true;
try {
this.settings = await this.daemonService.getSettings();
const isElectron = this.electronService.isElectron || (window as any).electronAPI != null;
const version = await this.daemonService.getVersion(isElectron);
const latestVersion = await this.daemonService.getLatestVersion();
this.currentVersion = version;
this.latestVersion = latestVersion;
await this.refreshLatestVersion();
await this.refreshCurrentVersion();
}
catch(error: any) {
console.error(error);

View file

@ -186,6 +186,152 @@ export class DaemonSettings {
return settings;
}
public toConfig(): string {
const commandOptions = this.toCommandOptions();
const lines: string[] = [];
commandOptions.forEach((commandOption: string, i: number, array: string[]) => {
if (commandOption.startsWith('--')) {
const next: string | undefined = array[i + 1];
if (!next || next.startsWith('--')) {
// option without parameter
lines.push(commandOption.includes('=') ? `${commandOption.replace('--', '')}` : `${commandOption.replace('--', '')}=1`);
}
else {
lines.push(`${commandOption.replace('--', '')}=${next}`);
}
}
});
return lines.join('\n');
}
public static parseConfig(configTxt: string): DaemonSettings {
const settings = new DaemonSettings();
const lines = configTxt.split('\n');
lines.forEach(line => {
line = line.trim();
// Ignore comments and empty lines
if (line.startsWith('#') || line === '') return;
const [key, value] = line.split('=').map(part => part.trim());
const boolValue = value === '1'; // Interpret 1 as true, 0 as false
switch (key) {
case 'data-dir': settings.dataDir = value; break;
case 'log-file': settings.logFile = value; break;
case 'log-level': settings.logLevel = parseInt(value, 10); break;
case 'max-log-files': settings.maxLogFileSize = parseInt(value, 10); break;
case 'max-log-file-size': settings.maxLogFileSize = parseInt(value, 10); break;
case 'no-igd': settings.noIgd = boolValue; break;
case 'enable-dns-blocklist': settings.enableDnsBlocklist = boolValue; break;
case 'testnet': settings.testnet = boolValue; break;
case 'mainnet': settings.mainnet = boolValue; break;
case 'stagenet': settings.stagenet = boolValue; break;
case 'offline': settings.offline = boolValue; break;
case 'limit-rate': settings.limitRate = parseInt(value, 10); break;
case 'limit-rate-up': settings.limitRateUp = parseInt(value, 10); break;
case 'limit-rate-down': settings.limitRateDown = parseInt(value, 10); break;
case 'proxy': settings.proxy = value; break;
case 'proxy-allow-dns-leaks': settings.proxyAllowDnsLeaks = boolValue; break;
case 'p2p-bind-ip': settings.p2pBindIp = value; break;
case 'p2p-bind-ipv6-address': settings.p2pBindIpv6Address = value; break;
case 'p2p-bind-port': settings.p2pBindPort = parseInt(value, 10); break;
case 'p2p-use-ipv6': settings.p2pUseIpv6 = boolValue; break;
case 'add-peer': settings.addPeer = value; break;
case 'add-priority-node': settings.addPriorityNode = value; break;
case 'bootstrap-daemon-address': settings.bootstrapDaemonAddress = value; break;
case 'bootstrap-daemon-login': settings.bootstrapDaemonLogin = value; break;
case 'bootstrap-daemon-proxy': settings.bootstrapDaemonProxy = value; break;
case 'rpc-bind-ip': settings.rpcBindIp = value; break;
case 'rpc-bind-port': settings.rpcBindPort = parseInt(value, 10); break;
case 'confirm-external-bind': settings.confirmExternalBind = boolValue; break;
case 'disable-dns-checkpoints': settings.disableDnsCheckpoints = boolValue; break;
case 'sync-pruned-blocks': settings.syncPrunedBlocks = boolValue; break;
case 'max-concurrency': settings.maxConcurrency = parseInt(value, 10); break;
case 'check-updates': settings.checkUpdates = value as 'disabled' | 'notify' | 'download' | 'update'; break;
case 'db-sync-mode': settings.dbSyncMode = value; break;
case 'db-salvage': settings.dbSalvage = boolValue; break;
case 'regtest': settings.regtest = boolValue; break;
case 'pad-transactions': settings.padTransactions = boolValue; break;
case 'anonymous-inbound': settings.anonymousInbound = value; break;
case 'fluffy-blocks': settings.fluffyBlocks = boolValue; break;
case 'no-fluffy-blocks': settings.noFluffyBlocks = boolValue; break;
case 'tx-proxy': settings.txProxy = value; break;
case 'max-txpool-weight': settings.maxTxPoolWeight = parseInt(value, 10); break;
case 'public-node': settings.publicNode = boolValue; break;
case 'allow-local-ip': settings.allowLocalIp = boolValue; break;
case 'tos-flag': settings.tosFlag = parseInt(value, 10); break;
case 'max-connections-per-ip': settings.maxConnectionsPerIp = parseInt(value, 10); break;
case 'disable-rpc-ban': settings.disableRpcBan = boolValue; break;
case 'rpc-access-control-origins': settings.rpcAccessControlOrigins = value; break;
case 'rpc-ssl': settings.rpcSsl = value as 'autodetect' | 'enabled' | 'disabled'; break;
case 'rpc-ssl-private-key': settings.rpcSslPrivateKey = value; break;
case 'rpc-ssl-certificate': settings.rpcSslCertificate = value; break;
case 'rpc-ssl-ca-certificates': settings.rpcSslCACertificates = value; break;
case 'rpc-ssl-allow-chained': settings.rpcSslAllowChained = boolValue; break;
case 'rpc-ssl-allow-any-cert': settings.rpcSslAllowAnyCert = boolValue; break;
case 'rpc-allowed-fingerprints': settings.rpcAllowedFingerprints = value; break;
case 'rpc-payment-allow-free-loopback': settings.rpcPaymentAllowFreeLoopback = boolValue; break;
case 'rpc-payment-difficulty': settings.rpcPaymentDifficuly = parseInt(value, 10); break;
case 'rpc-payment-credits': settings.rpcPaymentCredits = parseInt(value, 10); break;
case 'extra-messages-file': settings.extraMessagesFile = value; break;
case 'seed-node': settings.seedNode = value; break;
case 'zmq-rpc-bind-ip': settings.zmqRpcBindIp = value; break;
case 'zmq-rpc-bind-port': settings.zmqRpcBindPort = parseInt(value, 10); break;
case 'zmq-pub': settings.zmqPub = value; break;
case 'rpc-payment-address': settings.rpcPaymentAddress = value; break;
case 'no-zmq': settings.noZmq = boolValue; break;
case 'fixed-difficulty': settings.fixedDifficulty = parseInt(value, 10); break;
case 'prep-blocks-threads': settings.prepBlocksThreads = parseInt(value, 10); break;
case 'fast-block-sync': settings.fastBlockSync = boolValue; break;
case 'block-notify': settings.blockNotify = value; break;
case 'show-time-stats': settings.showTimeStats = boolValue; break;
case 'block-sync-size': settings.blockSyncSize = parseInt(value, 10); break;
case 'block-rate-notify': settings.blockRateNotify = value; break;
case 'reorg-notify': settings.reorgNotify = value; break;
case 'prune-blockchain': settings.pruneBlockchain = boolValue; break;
case 'keep-alt-blocks': settings.keepAltBlocks = boolValue; break;
case 'keep-fake-chain': settings.keepFakeChain = boolValue; break;
case 'add-exclusive-node': settings.addExclusiveNode = value; break;
case 'no-sync': settings.noSync = boolValue; break;
case 'start-mining': settings.startMining = value; break;
case 'mining-threads': settings.miningThreds = parseInt(value, 10); break;
case 'bg-mining-enable': settings.bgMiningEnable = boolValue; break;
case 'bg-mining-ignore-battery': settings.bgMiningIgnoreBattery = boolValue; break;
case 'bg-mining-idle-threshold': settings.bgMiningIdleThreshold = parseInt(value, 10); break;
case 'bg-mining-miner-target': settings.bgMiningMinerTarget = parseInt(value, 10); break;
case 'hide-my-port': settings.hideMyPort = boolValue; break;
case 'enforce-dns-checkpoint': settings.enforceDnsCheckpoint = boolValue; break;
case 'test-drop-download': settings.testDropDownload = boolValue; break;
case 'test-drop-download-height': settings.testDropDownloadHeight = parseInt(value, 10); break;
case 'test-dbg-lock-sleep': settings.testDbgLockSleep = parseInt(value, 10); break;
case 'in-peers': settings.inPeers = parseInt(value, 10); break;
case 'out-peers': settings.outPeers = parseInt(value, 10); break;
default: throw new Error(`Invalid setting: ${key}`);
}
});
return settings;
}
public static validateConfigFormat(confixTxt: string): boolean {
try {
DaemonSettings.parseConfig(confixTxt);
return true;
}
catch(error: any) {
console.warn(error);
return false;
}
}
public toCommandOptions(): string[] {
const options: string[] = [];
if (this.monerodPath != '') options.push(this.monerodPath);

View file

@ -98,10 +98,23 @@ declare global {
unregisterOnIsWifiConnectedResponse: () => void;
selectFolder: () => void;
selectFile: (extensions?: string[]) => void;
readFile: (filePath: string) => void;
unregisterOnReadFile: () => void;
onReadFile: (callback: (event: any, data: string) => void) => void;
onReadFileError: (callback: (event: any, error: string) => void) => void;
saveFile: (defaultPath: string, content: string) => void;
onSaveFile: (callback: (event: any, filePath: string) => void) => void;
onSaveFileError: (callback: (event: any, error: string) => void) => void;
unregisterOnSaveFile: () => void;
onSelectedFolder: (callback: (event: any, path: string) => void) => void;
onSelectedFile: (callback: (event: any, path: string) => void) => void;
unregisterOnSelectedFile: () => void;
unregisterOnSelectedFolder: () => void;
getPath: (path: 'home' | 'appData' | 'userData' | 'sessionData' | 'temp' | 'exe' | 'module' | 'desktop' | 'documents' | 'downloads' | 'music' | 'pictures' | 'videos' | 'recent' | 'logs' | 'crashDumps') => void;
onGetPath: (callback: (event: any, path: string) => void) => void;
unregisterOnGetPath: () => void;
getOsType: () => void;
gotOsType: (callback: (event: any, osType: { platform: string, arch: string }) => void) => void;
unregisterGotOsType: () => void;