From 07ebb1a86a26cb47ae7f557adfead369ac6c1002 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 24 Jul 2026 16:18:24 +0800 Subject: [PATCH 1/3] fix7 --- package-lock.json | 4 +- package.json | 2 +- src/app/bootstrap.js | 17 +++- src/app/common/app-props.js | 7 +- src/app/common/runtime-constants.js | 27 ++++- src/app/lib/single-instance.js | 4 +- src/app/lib/system-ca.js | 104 -------------------- src/app/server/child-process.js | 18 ---- src/app/server/ssh-known-hosts.js | 8 ++ src/app/widgets/widget-local-file-server.js | 2 +- src/app/widgets/widget-local-ftp-server.js | 2 +- 11 files changed, 61 insertions(+), 134 deletions(-) delete mode 100644 src/app/lib/system-ca.js diff --git a/package-lock.json b/package-lock.json index 72709a4..a828ec6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "electerm-harmony", - "version": "4.15.166", + "version": "4.15.167", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "electerm-harmony", - "version": "4.15.166", + "version": "4.15.167", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index eee684b..687d0ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "electerm-harmony", - "version": "4.15.166", + "version": "4.15.167", "description": "electerm — a free and open-source ssh/sftp/telnet/RDP/VNC/Spice/ftp client for HarmonyOS, built on the electerm codebase.", "main": "app.js", "bin": "npm/electerm", diff --git a/src/app/bootstrap.js b/src/app/bootstrap.js index 66bd4a0..27f2a12 100644 --- a/src/app/bootstrap.js +++ b/src/app/bootstrap.js @@ -76,8 +76,21 @@ function getDataPath () { blog('app.getPath("appData") failed:', e.message) } - // 4. Final fallback - blog('falling back to os.tmpdir():', os.tmpdir()) + // 4. Final fallback — try /data/local/tmp (writable on HarmonyOS/Android), + // then os.tmpdir() as the absolute last resort. + const fallbacks = ['/data/local/tmp', os.tmpdir()] + for (const dir of fallbacks) { + try { + fs.mkdirSync(dir, { recursive: true }) + blog('using fallback temp dir:', dir) + return dir + } catch (e) { + blog('fallback dir not writable:', dir, '-', e.message) + } + } + + // If everything fails, return os.tmpdir() anyway — better than crashing. + blog('all fallbacks failed, returning os.tmpdir():', os.tmpdir()) return os.tmpdir() } diff --git a/src/app/common/app-props.js b/src/app/common/app-props.js index cbf1cc0..e129e92 100644 --- a/src/app/common/app-props.js +++ b/src/app/common/app-props.js @@ -29,12 +29,15 @@ function getAppDataPath () { } const appDataPath = getAppDataPath() +const sshKeysPath = resolve(appDataPath, '.ssh') +// Create immediately so SSH key reads/writes never fail on a missing dir. +try { fs.mkdirSync(sshKeysPath, { recursive: true, mode: 0o700 }) } catch {} module.exports = { appPath: appDataPath, isPortable: false, exePath: '', - sshKeysPath: resolve(appDataPath, '.ssh'), - homeOrTmp: os.homedir(), + sshKeysPath, + homeOrTmp: constants.homeDir, ...constants } diff --git a/src/app/common/runtime-constants.js b/src/app/common/runtime-constants.js index 221bfa7..b1a0119 100644 --- a/src/app/common/runtime-constants.js +++ b/src/app/common/runtime-constants.js @@ -3,6 +3,7 @@ */ const os = require('os') +const fs = require('fs') const { resolve } = require('path') const platform = os.platform() @@ -36,6 +37,29 @@ const extIconPath = isDev const defaultUserName = require('./default-user-name') +/** + * On HarmonyOS, os.homedir() returns an inaccessible path + * (e.g. /storage/Users/currentUser) and os.tmpdir() may also point to + * a location outside the app sandbox. When process.env.DATA_PATH is set + * (by bootstrap.js), use it as the base for both home and temp dirs. + */ +function getHomeDir () { + if (process.env.DATA_PATH) { + return process.env.DATA_PATH + } + return os.homedir() +} + +function getTempDir () { + if (process.env.DATA_PATH) { + const dir = resolve(process.env.DATA_PATH, 'tmp') + // Create immediately so downstream writes never fail on a missing dir. + try { fs.mkdirSync(dir, { recursive: true }) } catch {} + return dir + } + return os.tmpdir() +} + module.exports = { isTest: !!NODE_TEST, isDev, @@ -50,6 +74,7 @@ module.exports = { minWindowWidth: 590, minWindowHeight: 400, defaultLang: 'en_us', - tempDir: require('os').tmpdir(), + homeDir: getHomeDir(), + tempDir: getTempDir(), packInfo: require(isDev ? '../../../package.json' : '../package.json') } diff --git a/src/app/lib/single-instance.js b/src/app/lib/single-instance.js index 77b2b1e..b414d05 100644 --- a/src/app/lib/single-instance.js +++ b/src/app/lib/single-instance.js @@ -5,12 +5,12 @@ const net = require('net') const fs = require('fs') const path = require('path') -const os = require('os') const { app } = require('electron') const globalState = require('./glob-state') +const { tempDir } = require('../common/runtime-constants') function getSocketPath () { - return path.join(os.tmpdir(), `${app.getName()}-instance.sock`) + return path.join(tempDir, `${app.getName()}-instance.sock`) } // Clean up stale socket file diff --git a/src/app/lib/system-ca.js b/src/app/lib/system-ca.js deleted file mode 100644 index aab8fd9..0000000 --- a/src/app/lib/system-ca.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Load system-trusted CA certificates into Node.js TLS store. - * Node.js uses its own bundled CA store and does not trust OS-level - * certificates by default. This module exports those certs so they - * can be passed to https.Agent as additional trusted CAs. - */ - -const { execSync } = require('child_process') -const { existsSync, readdirSync, readFileSync } = require('fs') -const { join } = require('path') -const os = require('os') - -let _certs = null - -function loadMacOS () { - try { - return execSync( - 'security find-certificate -a -p ' + - '/System/Library/Keychains/SystemRootCertificates.keychain ' + - '/Library/Keychains/System.keychain ' + - os.homedir() + '/Library/Keychains/login.keychain-db', - { encoding: 'utf8', timeout: 10000 } - ) - } catch { - return '' - } -} - -function loadLinux () { - const dirs = [ - '/etc/ssl/certs', - '/etc/pki/tls/certs', - '/etc/pki/ca-trust/extracted/pem', - '/usr/local/share/certs' - ] - const files = [] - for (const dir of dirs) { - if (existsSync(dir)) { - try { - for (const f of readdirSync(dir)) { - if (f.endsWith('.crt') || f.endsWith('.pem')) { - files.push(join(dir, f)) - } - } - break - } catch { /* skip */ } - } - } - if (!files.length) { - // fallback: try the ca-certificates bundle - const bundlePaths = [ - '/etc/ssl/certs/ca-certificates.crt', - '/etc/pki/tls/certs/ca-bundle.crt', - '/etc/ssl/ca-bundle.pem' - ] - for (const p of bundlePaths) { - if (existsSync(p)) { - return readFileSync(p, 'utf8') - } - } - return '' - } - return files.map(f => { - try { return readFileSync(f, 'utf8') } catch { return '' } - }).join('\n') -} - -function loadWindows () { - try { - return execSync( - 'powershell -Command ' + - '"Get-ChildItem -Path Cert:\\LocalMachine\\Root, Cert:\\LocalMachine\\CA, Cert:\\CurrentUser\\Root, Cert:\\CurrentUser\\CA ' + - '| Where-Object { $_.NotAfter -gt (Get-Date) } ' + - '| ForEach-Object { \'-----BEGIN CERTIFICATE-----\'; ' + - '[System.Convert]::ToBase64String($_.RawData, \'InsertLineBreaks\'); ' + - '\'-----END CERTIFICATE-----\' }"', - { encoding: 'utf8', timeout: 10000, windowsHide: true } - ) - } catch { - return '' - } -} - -function getSystemCAs () { - if (_certs !== null) { - return _certs - } - switch (os.platform()) { - case 'darwin': - _certs = loadMacOS() - break - case 'linux': - _certs = loadLinux() - break - case 'win32': - _certs = loadWindows() - break - default: - _certs = '' - } - return _certs -} - -module.exports = getSystemCAs diff --git a/src/app/server/child-process.js b/src/app/server/child-process.js index 0aa3551..032c111 100644 --- a/src/app/server/child-process.js +++ b/src/app/server/child-process.js @@ -7,12 +7,8 @@ */ const EventEmitter = require('events') -const { writeFileSync, unlinkSync } = require('fs') -const { tmpdir } = require('os') -const { join } = require('path') const log = require('../common/log') const dlog = require('../common/debug-logger') -const getSystemCAs = require('../lib/system-ca') // --use-system-ca is supported since Node.js 24.3.0 function supportsSystemCa () { @@ -38,14 +34,6 @@ module.exports = (config, env, sysLocale) => { process.env.NODE_OPTIONS = nodeOpts } - let extraCaFile - const systemCAs = getSystemCAs() - if (systemCAs) { - extraCaFile = join(tmpdir(), `electerm-system-ca-${Date.now()}.pem`) - writeFileSync(extraCaFile, systemCAs) - dlog('child-process: wrote system CA file:', extraCaFile) - } - // Create a mock child object for init-server.js compatibility const child = new EventEmitter() child.pid = process.pid @@ -81,12 +69,6 @@ module.exports = (config, env, sysLocale) => { }) } - if (extraCaFile) { - child.on('exit', () => { - try { unlinkSync(extraCaFile) } catch {} - }) - } - log.info('Server starting in-process, port:', config.port) return child } diff --git a/src/app/server/ssh-known-hosts.js b/src/app/server/ssh-known-hosts.js index 3ed223e..6a039b3 100644 --- a/src/app/server/ssh-known-hosts.js +++ b/src/app/server/ssh-known-hosts.js @@ -15,6 +15,14 @@ function normalizeHost (host = '') { } function getKnownHostsPath () { + // On HarmonyOS, os.homedir() returns an inaccessible path + // (e.g. /storage/Users/currentUser). Use the app's sandbox data + // directory (sshKeysPath, set by child-process.js from app-props.js) + // which is the same directory used for SSH keys. + const { sshKeysPath } = process.env + if (sshKeysPath) { + return join(sshKeysPath, 'known_hosts') + } return join(os.homedir(), '.ssh', 'known_hosts') } diff --git a/src/app/widgets/widget-local-file-server.js b/src/app/widgets/widget-local-file-server.js index daf3348..11ce8a9 100644 --- a/src/app/widgets/widget-local-file-server.js +++ b/src/app/widgets/widget-local-file-server.js @@ -26,7 +26,7 @@ const widgetInfo = { { name: 'directory', type: 'string', - default: os.homedir(), + default: process.env.DATA_PATH || os.homedir(), description: 'The directory to serve files from (default: user\'s home directory)' }, { diff --git a/src/app/widgets/widget-local-ftp-server.js b/src/app/widgets/widget-local-ftp-server.js index 429b91a..5b04025 100644 --- a/src/app/widgets/widget-local-ftp-server.js +++ b/src/app/widgets/widget-local-ftp-server.js @@ -25,7 +25,7 @@ const widgetInfo = { { name: 'directory', type: 'string', - default: os.homedir(), + default: process.env.DATA_PATH || os.homedir(), description: 'The directory to serve files from (default: user\'s home directory)' }, { From 51b30865a3715c7a4258c99675de609eca5b040b Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 24 Jul 2026 17:21:19 +0800 Subject: [PATCH 2/3] fix 8 --- package-lock.json | 26 ++-- package.json | 4 +- src/app/lib/db.js | 34 +---- src/app/lib/ipc.js | 8 +- src/app/lib/sqlite.js | 198 ----------------------------- src/app/migrate/index.js | 85 ------------- src/app/migrate/migrate-1-to-2.js | 120 ----------------- src/app/migrate/nedb-instance.js | 12 -- src/app/migrate/v1.25.0.js | 48 ------- src/app/migrate/v1.27.17.js | 66 ---------- src/app/migrate/v1.3.0.js | 107 ---------------- src/app/migrate/v1.3.9.js | 44 ------- src/app/migrate/v1.32.36.js | 44 ------- src/app/migrate/v1.34.20.js | 43 ------- src/app/migrate/v1.34.59.js | 32 ----- src/app/migrate/v1.5.13.js | 57 --------- src/app/migrate/v1.7.0.js | 36 ------ src/app/migrate/version-upgrade.js | 39 ------ 18 files changed, 22 insertions(+), 981 deletions(-) delete mode 100644 src/app/lib/sqlite.js delete mode 100644 src/app/migrate/index.js delete mode 100644 src/app/migrate/migrate-1-to-2.js delete mode 100644 src/app/migrate/nedb-instance.js delete mode 100644 src/app/migrate/v1.25.0.js delete mode 100644 src/app/migrate/v1.27.17.js delete mode 100644 src/app/migrate/v1.3.0.js delete mode 100644 src/app/migrate/v1.3.9.js delete mode 100644 src/app/migrate/v1.32.36.js delete mode 100644 src/app/migrate/v1.34.20.js delete mode 100644 src/app/migrate/v1.34.59.js delete mode 100644 src/app/migrate/v1.5.13.js delete mode 100644 src/app/migrate/v1.7.0.js delete mode 100644 src/app/migrate/version-upgrade.js diff --git a/package-lock.json b/package-lock.json index a828ec6..1c6f8e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "electerm-harmony", - "version": "4.15.167", + "name": "electerm", + "version": "3.15.167", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "electerm-harmony", - "version": "4.15.167", + "name": "electerm", + "version": "3.15.167", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -45,7 +45,7 @@ "zmodem2": "1.4.0" }, "bin": { - "electerm-harmony": "npm/electerm" + "electerm": "npm/electerm" }, "devDependencies": { "@ant-design/icons": "6.2.5", @@ -53,7 +53,7 @@ "@electerm/electerm-resource": "2.1.9", "@fontsource/maple-mono": "^5.2.5", "@novnc/novnc": "1.7.0", - "@types/node": "22.9.3", + "@types/node": "22.12.0", "@vitejs/plugin-react": "^5.2.0", "@xterm/addon-attach": "0.13.0-beta.288", "@xterm/addon-fit": "0.12.0-beta.288", @@ -2313,13 +2313,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.3.tgz", - "integrity": "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==", + "version": "22.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz", + "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/responselike": { @@ -11059,9 +11059,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 687d0ae..ac33716 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "electerm-harmony", - "version": "4.15.167", + "name": "electerm", + "version": "3.15.167", "description": "electerm — a free and open-source ssh/sftp/telnet/RDP/VNC/Spice/ftp client for HarmonyOS, built on the electerm codebase.", "main": "app.js", "bin": "npm/electerm", diff --git a/src/app/lib/db.js b/src/app/lib/db.js index 0536b3d..f54d5d1 100644 --- a/src/app/lib/db.js +++ b/src/app/lib/db.js @@ -1,7 +1,6 @@ /** * db loader - * Provides electron-related environment to nedb/sqlite modules - * Falls back to nedb (pure JS) if node:sqlite is not available (e.g. HarmonyOS) + * Uses nedb (pure JS, no native dependencies). */ const { appPath, defaultUserName } = require('../common/app-props') @@ -10,31 +9,8 @@ const dlog = require('../common/debug-logger') const encOpts = { enc: safeEncrypt, dec: safeDecrypt } -function trySqlite () { - dlog('db.js: trying node:sqlite...') - try { - require('node:sqlite') - const { createDb } = require('./sqlite') - dlog('db.js: node:sqlite available, creating db...') - const result = createDb(appPath, defaultUserName, encOpts) - dlog('db.js: sqlite db created') - return result - } catch (e) { - dlog('db.js: node:sqlite not available:', e?.message || e) - console.warn('node:sqlite not available, falling back to nedb:', e?.message || e) - return null - } -} - -let db = null -dlog('db.js: node version:', process.versions.node) -if (process.versions.node >= '22.0.0') { - db = trySqlite() -} -if (!db) { - dlog('db.js: using nedb...') - const { createDb } = require('./nedb') - db = createDb(appPath, defaultUserName, encOpts) - dlog('db.js: nedb db created') -} +dlog('db.js: using nedb') +const { createDb } = require('./nedb') +const db = createDb(appPath, defaultUserName, encOpts) +dlog('db.js: nedb db created') module.exports = db diff --git a/src/app/lib/ipc.js b/src/app/lib/ipc.js index 94e2a77..f946efd 100644 --- a/src/app/lib/ipc.js +++ b/src/app/lib/ipc.js @@ -24,10 +24,6 @@ const { stopWidget, runWidgetFunc } = require('../widgets/load-widget') -const { - checkMigrate, - migrate -} = require('../migrate/migrate-1-to-2') const { setPassword, checkPassword @@ -176,8 +172,6 @@ function initIpc () { loadFontList, doUpgrade, checkDbUpgrade, - checkMigrate, - migrate, getExitStatus: () => globalState.get('exitStatus'), setExitStatus: (status) => { globalState.set('exitStatus', status) @@ -243,6 +237,8 @@ function initIpc () { unregisterDeepLink, checkProtocolRegistration, getPendingDeepLink, + checkMigrate: () => false, + migrate: () => false, getEnv: (key) => { if (key) { return SAFE_ENV_KEYS.includes(key) ? process.env[key] : '' diff --git a/src/app/lib/sqlite.js b/src/app/lib/sqlite.js deleted file mode 100644 index 7d36cfc..0000000 --- a/src/app/lib/sqlite.js +++ /dev/null @@ -1,198 +0,0 @@ -/** - * sqlite api wrapper - * Updated to use two database files: one for 'data' table, one for others - * Accepts appPath and defaultUserName as parameters to avoid electron dependency - */ - -const { resolve } = require('path') -const fs = require('fs') -const uid = require('../common/uid') -const { DatabaseSync } = require('node:sqlite') - -// Tables whose stored data values should be encrypted at rest -const ENC_TABLES = new Set(['bookmarks', 'profiles', 'data', 'history', 'terminalCommandHistory', 'aiChatHistory']) - -// Within the 'data' table, only this specific record is encrypted -const DATA_ENC_ID = 'userConfig' - -// Prefix added to stored strings to mark them as encrypted -const ENC_PREFIX = 'enc:' - -function createDb (appPath, defaultUserName, { enc, dec } = {}) { - const appDataPath = process.env.DATA_PATH || resolve(appPath, 'electerm') - - if (!fs.existsSync(appDataPath)) { - fs.mkdirSync(appDataPath, { recursive: true }) - } - - // Define database folder and paths for two database files - const dbFolder = resolve(appDataPath, 'users', defaultUserName) - const mainDbPath = resolve(dbFolder, 'electerm.db') - const dataDbPath = resolve(dbFolder, 'electerm_data.db') - - // Ensure parent directory exists - if (!fs.existsSync(dbFolder)) { - fs.mkdirSync(dbFolder, { recursive: true }) - } - // Create two database instances - const mainDb = new DatabaseSync(mainDbPath) - const dataDb = new DatabaseSync(dataDbPath) - - const tables = [ - 'bookmarks', - 'bookmarkGroups', - 'addressBookmarks', - 'terminalThemes', - 'lastStates', - 'data', - 'quickCommands', - 'log', - 'dbUpgradeLog', - 'profiles', - 'workspaces', - 'history', - 'terminalCommandHistory', - 'aiChatHistory', - 'autoRunWidgets' - ] - - // Create tables in appropriate databases - for (const table of tables) { - if (table === 'data') { - dataDb.exec(`CREATE TABLE IF NOT EXISTS \`${table}\` (_id TEXT PRIMARY KEY, data TEXT)`) - } else { - mainDb.exec(`CREATE TABLE IF NOT EXISTS \`${table}\` (_id TEXT PRIMARY KEY, data TEXT)`) - } - } - - // Helper function to get the appropriate database for a table - function getDatabase (dbName) { - return dbName === 'data' ? dataDb : mainDb - } - - /** - * Encrypt a plain JSON string for storage. - * Returns the original string when encryption is not configured. - */ - function encryptData (jsonStr) { - if (!enc) return jsonStr - return ENC_PREFIX + enc(jsonStr) - } - - /** - * Decrypt a stored string back to plain JSON. - * Returns the original string when decryption is not configured or the - * value was stored without encryption. - */ - function decryptData (stored) { - if (!dec || !stored) return stored - if (!stored.startsWith(ENC_PREFIX)) return stored - return dec(stored.slice(ENC_PREFIX.length)) - } - - function shouldEncForRow (dbName, id) { - if (dbName === 'data') return id === DATA_ENC_ID - return ENC_TABLES.has(dbName) - } - - function toDoc (row, dbName) { - if (!row) return null - const shouldDec = dec && shouldEncForRow(dbName, row._id) - const raw = shouldDec ? decryptData(row.data) : row.data - let r = {} - try { - r = JSON.parse(raw || '{}') - } catch (e) { - console.error(`Error parsing JSON for row ${row._id}:`, e.message) - } - return { - ...r, - _id: row._id - } - } - - function toRow (doc, dbName) { - const _id = doc._id || doc.id || uid() - const copy = { ...doc } - delete copy._id - delete copy.id - const jsonStr = JSON.stringify(copy) - return { - _id, - data: enc && shouldEncForRow(dbName, _id) ? encryptData(jsonStr) : jsonStr - } - } - - async function dbAction (dbName, op, ...args) { - if (op === 'compactDatafile') { - return - } - if (!tables.includes(dbName)) { - throw new Error(`Table ${dbName} does not exist`) - } - - // Get the appropriate database for this table - const db = getDatabase(dbName) - - if (op === 'find') { - const sql = `SELECT * FROM \`${dbName}\`` - const stmt = db.prepare(sql) - const rows = stmt.all() - return (rows || []).map(row => toDoc(row, dbName)).filter(Boolean) - } else if (op === 'findOne') { - const query = args[0] || {} - const sql = `SELECT * FROM \`${dbName}\` WHERE _id = ? LIMIT 1` - const params = [query._id] - const stmt = db.prepare(sql) - const row = stmt.get(...params) - return toDoc(row, dbName) - } else if (op === 'insert') { - const inserts = Array.isArray(args[0]) ? args[0] : [args[0]] - const inserted = [] - for (const doc of inserts) { - const { _id, data } = toRow(doc, dbName) - const stmt = db.prepare(`INSERT OR REPLACE INTO \`${dbName}\` (_id, data) VALUES (?, ?)`) - stmt.run(_id, data) - inserted.push({ ...doc, _id }) - } - return Array.isArray(args[0]) ? inserted : inserted[0] - } else if (op === 'remove') { - const query = args[0] || {} - const sql = `DELETE FROM \`${dbName}\` WHERE _id = ?` - const params = [query._id] - const stmt = db.prepare(sql) - const res = stmt.run(...params) - return res.changes - } else if (op === 'update') { - const query = args[0] - const updateObj = args[1] - const options = args[2] || {} - const { upsert = false } = options - const qid = query._id || query.id - const newData = updateObj.$set || updateObj - const { _id, data } = toRow({ - _id: qid, - ...newData - }, dbName) - let stmt - let res - if (upsert) { - stmt = db.prepare(`REPLACE INTO \`${dbName}\` (_id, data) VALUES (?, ?)`) - res = stmt.run(_id, data) - } else { - stmt = db.prepare(`UPDATE \`${dbName}\` SET data = ? WHERE _id = ?`) - res = stmt.run(data, qid) - } - return res.changes - } - } - - return { - dbAction, - tables - } -} - -module.exports = { - createDb -} diff --git a/src/app/migrate/index.js b/src/app/migrate/index.js deleted file mode 100644 index 7f3452a..0000000 --- a/src/app/migrate/index.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * common data upgrade process - * It will check current version in db and check version in package.json, - * run every upgrade script one by one - */ - -const { packInfo } = require('../common/app-props') -const { version: packVersion } = packInfo -const { resolve } = require('path') -const fs = require('fs') -const log = require('../common/log') -const compare = require('../common/version-compare') -const { dbAction } = require('./nedb-instance') -const _ = require('../lib/lodash.js') -const { updateDBVersion } = require('./version-upgrade') -const emptyVersion = '0.0.0' -const versionQuery = { - _id: 'version' -} - -async function getDBVersion () { - const version = await dbAction('data', 'findOne', versionQuery) - .then(doc => { - return doc ? doc.value : emptyVersion - }) - .catch(e => { - log.error(e) - return emptyVersion - }) - return version -} - -/** - * get upgrade versions should be run as version upgrade - */ -async function getUpgradeVersionList () { - const version = await getDBVersion() - const list = fs.readdirSync(__dirname) - return list.filter(f => { - const vv = f.replace('.js', '').replace('v', '') - return /^v\d/.test(f) && compare(vv, version) > 0 && compare(vv, packVersion) <= 0 - }).sort((a, b) => { - return compare(a, b) - }) -} -async function versionShouldUpgrade () { - const dbVersion = await getDBVersion() - log.info('database version:', dbVersion) - return compare(dbVersion, packVersion) < 0 -} - -async function shouldUpgrade () { - const shouldUpgradeVersion = await versionShouldUpgrade() - if (!shouldUpgradeVersion) { - return false - } - const dbVersion = await getDBVersion() - log.info('dbVersion', dbVersion) - if (dbVersion === emptyVersion) { - return false - } - const list = await getUpgradeVersionList() - if (_.isEmpty(list)) { - await updateDBVersion(packVersion) - return false - } - return { - dbVersion, - packVersion - } -} - -async function doUpgrade () { - const list = await getUpgradeVersionList() - log.info('Upgrading...') - for (const v of list) { - const p = resolve(__dirname, v) - const run = require(p) - await run() - } - log.info('Upgrade end') -} - -exports.checkDbUpgrade = shouldUpgrade -exports.doUpgrade = doUpgrade diff --git a/src/app/migrate/migrate-1-to-2.js b/src/app/migrate/migrate-1-to-2.js deleted file mode 100644 index b78c278..0000000 --- a/src/app/migrate/migrate-1-to-2.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * migrate from NeDB (v1) to SQLite (v2) - */ - -const { resolve } = require('path') -const { existsSync, renameSync } = require('fs') -const { appPath, defaultUserName } = require('../common/app-props') -const log = require('../common/log') - -const reso = (name) => { - return resolve(appPath, 'electerm', 'users', defaultUserName, `electerm.${name}.nedb`) -} - -const tables = [ - 'bookmarks', - 'bookmarkGroups', - 'addressBookmarks', - 'terminalThemes', - 'lastStates', - 'data', - 'quickCommands', - 'log', - 'dbUpgradeLog', - 'profiles' -] - -/** - * Check if migration from v1 (NeDB) to v2 (SQLite) is needed - * @returns {boolean} true if migration is needed - */ -function checkMigrate () { - if (process.versions.node < '22.0.0') { - return false - } - // Check if any NeDB files exist - for (const table of tables) { - const nedbPath = reso(table) - if (existsSync(nedbPath)) { - return true - } - } - return false -} - -/** - * Migrate all data from NeDB to SQLite and backup NeDB files - */ -async function migrate () { - log.info('Starting migration from NeDB (v1) to SQLite (v2)...') - // nedb-instance: raw nedb without enc/dec (legacy data was never encrypted) - const { dbAction: nedbDbAction } = require('./nedb-instance') - // Use plain sqlite (no enc/dec) for migration writes: safeStorage encryption - // is not reliable across restarts in the IPC/migration context. The app's - // normal read/write path will encrypt data on the next user-triggered write. - const { appPath, defaultUserName } = require('../common/app-props') - const { createDb: createSqlite } = require('../lib/sqlite') - const { dbAction: sqliteDbAction } = createSqlite(appPath, defaultUserName) - const { - checkDbUpgrade, - doUpgrade - } = require('./index') - if (await checkDbUpgrade()) { - await doUpgrade() - } - - // Migrate data from each table - for (const table of tables) { - const nedbPath = reso(table) - - if (existsSync(nedbPath)) { - log.info(`Migrating table: ${table}`) - - // Read all data from NeDB (unencrypted legacy data) - const nedbData = await nedbDbAction(table, 'find', {}) - - if (nedbData && nedbData.length > 0) { - log.info(`Found ${nedbData.length} records in ${table}`) - - // Insert/update data into SQLite via db.js so enc/dec is applied - for (const record of nedbData) { - // Ensure record has an _id field - const recordId = record._id || record.id - if (!recordId) { - log.warn(`Record in ${table} has no _id or id field, skipping:`, record) - continue - } - // Use update with upsert option to handle existing records gracefully - await sqliteDbAction(table, 'update', - { _id: recordId }, - { $set: record }, - { upsert: true } - ) - } - - log.info(`Successfully migrated ${nedbData.length} records from ${table}`) - } else { - log.info(`Table ${table} is empty, nothing to migrate`) - } - - // Rename NeDB file to .bak - const backupPath = nedbPath + '.bak' - try { - renameSync(nedbPath, backupPath) - log.info(`Backed up ${nedbPath} to ${backupPath}`) - } catch (renameError) { - log.error(`Error backing up ${nedbPath}:`, renameError) - } - } else { - log.info(`NeDB file for ${table} does not exist, skipping`) - } - } - - log.info('Migration from NeDB to SQLite completed successfully') - return true -} - -module.exports = { - checkMigrate, - migrate -} diff --git a/src/app/migrate/nedb-instance.js b/src/app/migrate/nedb-instance.js deleted file mode 100644 index 500e2ca..0000000 --- a/src/app/migrate/nedb-instance.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Shared NeDB instance for migrate scripts. - * Intentionally created without enc/dec because legacy NeDB data was never - * encrypted and these scripts must be able to read/write the raw nedb files - * during the pre-migration upgrade phase. - */ - -const { appPath, defaultUserName } = require('../common/app-props') -const { createDb } = require('../lib/nedb') - -module.exports = createDb(appPath, defaultUserName) -// exports: { dbAction, tables } diff --git a/src/app/migrate/v1.25.0.js b/src/app/migrate/v1.25.0.js deleted file mode 100644 index 0eaba6f..0000000 --- a/src/app/migrate/v1.25.0.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * upgrade database to v1.7.0 - */ - -const { userConfigId } = require('../common/constants') -const { dbAction } = require('./nedb-instance') -const { updateDBVersion } = require('./version-upgrade') -const log = require('../common/log') - -async function fixAll () { - const q = { - _id: userConfigId - } - const conf = await dbAction('data', 'findOne', q) - if (!conf) { - return - } - if (!conf.syncSetting) { - conf.syncSetting = {} - } - const { - syncEncrypt, - githubAccessToken, - giteeAccessToken - } = conf.syncSetting - if (!syncEncrypt) { - return - } - if (githubAccessToken) { - conf.syncSetting.githubSyncPassword = githubAccessToken - } - if (giteeAccessToken) { - conf.syncSetting.giteeSyncPassword = giteeAccessToken - } - delete conf.syncSetting.syncEncrypt - await dbAction('data', 'update', q, { - ...q, - ...conf - }) -} - -module.exports = async () => { - const versionTo = '1.25.0' - log.info(`Start: upgrading to v${versionTo}`) - await fixAll() - await updateDBVersion(versionTo) - log.info(`Done: upgrading to v${versionTo}`) -} diff --git a/src/app/migrate/v1.27.17.js b/src/app/migrate/v1.27.17.js deleted file mode 100644 index 5058ce1..0000000 --- a/src/app/migrate/v1.27.17.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * upgrade database to v1.27.7 - */ - -const { userConfigId } = require('../common/constants') -const { dbAction } = require('./nedb-instance') -const { updateDBVersion } = require('./version-upgrade') -const log = require('../common/log') -const { buildProxyString } = require('../lib/build-proxy') - -async function fixConf () { - log.info('Start update global proxy config') - const q = { - _id: userConfigId - } - const conf = await dbAction('data', 'findOne', q) - if (!conf) { - return - } - const proxy = buildProxyString(conf) - if (proxy) { - conf.proxy = proxy - } - const props = [ - 'proxyPort', 'proxyType', 'proxyIp', 'proxyUsername', 'proxyPassword' - ] - for (const p of props) { - delete conf[p] - } - await dbAction('data', 'update', q, { - ...q, - ...conf - }) -} - -async function fixBookmarks () { - log.info('Start update bookmark proxy config') - const arr = await dbAction('bookmarks', 'find', {}) - const len = arr.length - let i = 0 - log.info('bookmarks count:', len) - for (const b of arr) { - const proxy = buildProxyString(b.proxy || {}) - console.log(i + 1, b._id, proxy) - await dbAction('bookmarks', 'update', { - _id: b._id - }, { - ...b, - proxy - }) - i = i + 1 - } -} - -async function fixAll () { - await fixConf() - await fixBookmarks() -} - -module.exports = async () => { - const versionTo = '1.27.17' - log.info(`Start: upgrading to v${versionTo}`) - await fixAll() - await updateDBVersion(versionTo) - log.info(`Done: upgrading to v${versionTo}`) -} diff --git a/src/app/migrate/v1.3.0.js b/src/app/migrate/v1.3.0.js deleted file mode 100644 index 6a83bf6..0000000 --- a/src/app/migrate/v1.3.0.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * upgrade database to v1.3.0 - * migrate old file based db to nedb - */ - -const { resolve } = require('path') -const { dbAction, tables } = require('./nedb-instance') -const { appPath } = require('../common/app-props') -const userConfigPath = resolve(appPath, 'electerm-user-config.json') -const savePath = resolve(appPath, 'electerm-localstorage.json') -const { existsSync, unlinkSync, writeFileSync } = require('fs') -const log = require('../common/log') -const _ = require('../lib/lodash.js') -const { userConfigId } = require('../common/constants') -const { updateDBVersion } = require('./version-upgrade') - -async function loadArr (arr, name) { - await dbAction(name, 'insert', arr.map(d => { - const { id, ...rest } = d - return { - _id: id, - ...rest - } - })).catch(log.error) -} - -function shouldLoadAsArray (key, value) { - return tables.includes(key) && _.isArray(value) && value.length && value[0].id -} - -async function migrateData () { - const exist = existsSync(savePath) - if (!exist) { - return false - } - log.log('Start migrating data') - let json = {} - try { - json = require(savePath) - } catch (e) { - log.error(e) - log.error('load json data fails') - } - const keys = Object.keys(json) - for (const k of keys) { - const v = json[k] - const _id = k.startsWith('laststate') - ? k.split('.')[1] - : k - const db = k.startsWith('laststate') - ? 'lastStates' - : 'data' - if (k === 'themes') { - const vs = Object.values(v) - for (const vv of vs) { - const { id, ...rest } = vv - await dbAction('terminalThemes', 'insert', { - _id: id, - ...rest - }).catch(log.error) - } - } else if (shouldLoadAsArray(k, v)) { - await loadArr(v, k) - } else { - await dbAction(db, 'insert', { - _id, - value: v - }).catch(log.error) - } - } - await writeFileSync(savePath + '.backup', JSON.stringify(json)) - await unlinkSync(savePath) - log.log('Finish migrating data') -} - -async function migrateUserConfig () { - const exist = existsSync(userConfigPath) - if (!exist) { - return false - } - log.log('Start migrating user config') - let uf = {} - try { - uf = require(userConfigPath) - } catch (e) { - log.error(e) - log.error('load user config fails') - } - await dbAction('data', 'update', { - _id: userConfigId - }, { - _id: userConfigId, - value: uf - }, { - upsert: true - }).catch(log.error) - log.log('End migrating user config') -} - -module.exports = async () => { - const versionTo = '1.3.0' - log.info(`Start: upgrading to v${versionTo}`) - await migrateData() - await migrateUserConfig() - await updateDBVersion(versionTo) - log.info(`Done: upgrading to v${versionTo}`) -} diff --git a/src/app/migrate/v1.3.9.js b/src/app/migrate/v1.3.9.js deleted file mode 100644 index 44b3578..0000000 --- a/src/app/migrate/v1.3.9.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * upgrade database to v1.3.9 - * migrate old file based db to nedb - */ - -const { dbAction, tables } = require('./nedb-instance') -const { updateDBVersion } = require('./version-upgrade') -const log = require('../common/log') - -function wait (time) { - return new Promise(resolve => { - setTimeout(resolve, time) - }) -} - -async function fixAll () { - for (const name of tables) { - const all = await dbAction(name, 'find', {}) - for (const inst of all) { - const { id, _id, ...rest } = inst - if (id) { - await dbAction(name, 'remove', { - id - }) - await dbAction(name, 'remove', { - _id: id - }, { multi: true }) - await wait(100) - await dbAction(name, 'insert', { - _id: id, - ...rest - }) - } - } - } -} - -module.exports = async () => { - const versionTo = '1.3.9' - log.info(`Start: upgrading to v${versionTo}`) - await fixAll() - await updateDBVersion(versionTo) - log.info(`Done: upgrading to v${versionTo}`) -} diff --git a/src/app/migrate/v1.32.36.js b/src/app/migrate/v1.32.36.js deleted file mode 100644 index 0742247..0000000 --- a/src/app/migrate/v1.32.36.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * upgrade database to v1.32.36 - */ - -const { dbAction } = require('./nedb-instance') -const { updateDBVersion } = require('./version-upgrade') -const log = require('../common/log') -const { buildSshTunnels } = require('../common/build-ssh-tunnel') - -async function fixBookmarks () { - log.info('Start update bookmark Ssh Tunnels config') - const arr = await dbAction('bookmarks', 'find', {}) - const len = arr.length - let i = 0 - log.info('bookmarks count:', len) - for (const b of arr) { - console.log(i + 1, b._id, b.sshTunnel ? 'has sshTunnel' : 'no sshTunnel') - if (b.sshTunnel) { - const sshTunnels = buildSshTunnels(b) - delete b.sshTunnel - delete b.sshTunnelRemotePort - delete b.sshTunnelLocalPort - await dbAction('bookmarks', 'update', { - _id: b._id - }, { - ...b, - sshTunnels - }) - } - i = i + 1 - } -} - -async function fixAll () { - await fixBookmarks() -} - -module.exports = async () => { - const versionTo = '1.32.36' - log.info(`Start: upgrading to v${versionTo}`) - await fixAll() - await updateDBVersion(versionTo) - log.info(`Done: upgrading to v${versionTo}`) -} diff --git a/src/app/migrate/v1.34.20.js b/src/app/migrate/v1.34.20.js deleted file mode 100644 index f425d7e..0000000 --- a/src/app/migrate/v1.34.20.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * upgrade database to v1.34.20 - */ - -const { dbAction } = require('./nedb-instance') -const { updateDBVersion } = require('./version-upgrade') -const log = require('../common/log') -const { buildRunScripts } = require('../common/build-run-scripts') - -async function fixBookmarks () { - log.info('Start update bookmark loginScript config') - const arr = await dbAction('bookmarks', 'find', {}) - const len = arr.length - let i = 0 - log.info('bookmarks count:', len) - for (const b of arr) { - console.log(i + 1, b._id, b.loginScript ? 'has loginScript' : 'no loginScript') - if (b.loginScript) { - const runScripts = buildRunScripts(b) - delete b.loginScript - delete b.loginScriptDelay - await dbAction('bookmarks', 'update', { - _id: b._id - }, { - ...b, - runScripts - }) - } - i = i + 1 - } -} - -async function fixAll () { - await fixBookmarks() -} - -module.exports = async () => { - const versionTo = '1.34.20' - log.info(`Start: upgrading to v${versionTo}`) - await fixAll() - await updateDBVersion(versionTo) - log.info(`Done: upgrading to v${versionTo}`) -} diff --git a/src/app/migrate/v1.34.59.js b/src/app/migrate/v1.34.59.js deleted file mode 100644 index 56340aa..0000000 --- a/src/app/migrate/v1.34.59.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * upgrade database to v1.34.20 - */ - -const { dbAction } = require('./nedb-instance') -const { userConfigId } = require('../common/constants') -const { updateDBVersion } = require('./version-upgrade') -const log = require('../common/log') - -async function fixAll () { - log.info('Start update default terminal word separator config') - const q = { - _id: userConfigId - } - const conf = await dbAction('data', 'findOne', q) - if (conf && conf.terminalWordSeparator && !conf.terminalWordSeparator.includes(' ')) { - conf.terminalWordSeparator = conf.terminalWordSeparator.slice(0, 1) + ' ' + - conf.terminalWordSeparator.slice(1) - await dbAction('data', 'update', q, { - ...q, - ...conf - }) - } -} - -module.exports = async () => { - const versionTo = '1.34.59' - log.info(`Start: upgrading to v${versionTo}`) - await fixAll() - await updateDBVersion(versionTo) - log.info(`Done: upgrading to v${versionTo}`) -} diff --git a/src/app/migrate/v1.5.13.js b/src/app/migrate/v1.5.13.js deleted file mode 100644 index 63409a2..0000000 --- a/src/app/migrate/v1.5.13.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * upgrade database to v1.5.13 - */ - -const { userConfigId } = require('../common/constants') -const { dbAction } = require('./nedb-instance') -const { updateDBVersion } = require('./version-upgrade') -const log = require('../common/log') -const { decrypt } = require('../lib/enc') - -async function fixAll () { - const q = { - _id: userConfigId - } - const conf = await dbAction('data', 'findOne', q) - if (!conf.syncSetting) { - conf.syncSetting = {} - } - const { - encrypted, - gistId, - githubAccessToken, - lastSyncTime - } = conf.syncSetting - if (lastSyncTime) { - conf.syncSetting.githubLastSyncTime = lastSyncTime - } - if (gistId) { - conf.syncSetting.githubGistId = gistId - } - if ( - encrypted && - gistId && - githubAccessToken - ) { - const nt = decrypt( - conf.syncSetting.githubAccessToken, - conf.syncSetting.gistId - ) - conf.syncSetting.githubAccessToken = nt - } - delete conf.syncSetting.encrypted - delete conf.syncSetting.lastSyncTime - delete conf.syncSetting.gistId - await dbAction('data', 'update', q, { - ...q, - ...conf - }) -} - -module.exports = async () => { - const versionTo = '1.5.13' - log.info(`Start: upgrading to v${versionTo}`) - await fixAll() - await updateDBVersion(versionTo) - log.info(`Done: upgrading to v${versionTo}`) -} diff --git a/src/app/migrate/v1.7.0.js b/src/app/migrate/v1.7.0.js deleted file mode 100644 index c1d0894..0000000 --- a/src/app/migrate/v1.7.0.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * upgrade database to v1.7.0 - */ - -const { dbAction } = require('./nedb-instance') -const { updateDBVersion } = require('./version-upgrade') -const log = require('../common/log') -const defaults = require('./db-defaults') - -async function fixAll () { - const defaultThemeConfig = defaults[0].data[0] - const defaultLightThemeConfig = defaults[0].data[1] - const all = await dbAction('terminalThemes', 'find', {}).catch(log.error) || [] - for (const item of all) { - const q = { - _id: item.id || item._id - } - const updates = q._id === 'default' - ? defaultThemeConfig - : { - ...item, - uiThemeConfig: defaultThemeConfig.uiThemeConfig - } - await dbAction('terminalThemes', 'update', q, updates).catch(log.error) - } - await dbAction('terminalThemes', 'insert', defaultLightThemeConfig).catch(log.error) - log.info('end: update db') -} - -module.exports = async () => { - const versionTo = '1.7.0' - log.info(`Start: upgrading to v${versionTo}`) - await fixAll() - await updateDBVersion(versionTo) - log.info(`Done: upgrading to v${versionTo}`) -} diff --git a/src/app/migrate/version-upgrade.js b/src/app/migrate/version-upgrade.js deleted file mode 100644 index 4a53fd6..0000000 --- a/src/app/migrate/version-upgrade.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * upgrade db version - */ - -/** - * common data upgrade process - * It will check current version in db and check version in package.json, - * run every upgrade script one by one - */ - -const log = require('../common/log') -const { dbAction } = require('./nedb-instance') - -async function updateDBVersion (toVersion) { - const versionQuery = { - _id: 'version' - } - log.info('upgrade db version to', toVersion) - await dbAction('data', 'update', versionQuery, { - ...versionQuery, - value: toVersion - }, { - upsert: true - }) - .catch(e => { - log.error(e) - log.error('upgrade db version error', toVersion) - }) - await dbAction('dbUpgradeLog', 'insert', { - time: Date.now(), - toVersion - }) - .catch(e => { - log.error(e) - log.error('insert dbUpgradeLog error', toVersion) - }) -} - -exports.updateDBVersion = updateDBVersion From e2494c28635f88cf5840dafef9980aadb271bf81 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 24 Jul 2026 18:02:45 +0800 Subject: [PATCH 3/3] Fix version --- AppScope/app.json5 | 4 ++-- entry/oh-package.json5 | 2 +- oh-package.json5 | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AppScope/app.json5 b/AppScope/app.json5 index b9df3d8..61e398b 100644 --- a/AppScope/app.json5 +++ b/AppScope/app.json5 @@ -2,8 +2,8 @@ "app": { "bundleName": "org.electerm.electerm", "vendor": "electerm", - "versionCode": 41500121, - "versionName": "4.15.121", + "versionCode": 31500167, + "versionName": "3.15.167", "icon": "$media:app_icon", "label": "$string:app_name" } diff --git a/entry/oh-package.json5 b/entry/oh-package.json5 index 1c403c8..8d43aea 100644 --- a/entry/oh-package.json5 +++ b/entry/oh-package.json5 @@ -1,6 +1,6 @@ { "name": "entry", - "version": "4.15.121", + "version": "3.15.167", "description": "Electerm HarmonyOS entry module — provides UI surface and Electron runtime integration", "main": "", "license": "MIT", diff --git a/oh-package.json5 b/oh-package.json5 index b4655a9..2a1f240 100644 --- a/oh-package.json5 +++ b/oh-package.json5 @@ -1,6 +1,6 @@ { "name": "electerm-harmony", - "version": "4.15.121", + "version": "3.15.167", "description": "Free and open-sourced ssh/sftp/telnet/RDP/VNC/Spice/ftp client for HarmonyOS", "main": "", "license": "MIT",