sets up config service codex-config-file saving

This commit is contained in:
Ben 2025-04-14 11:27:58 +02:00
parent 4341a67ebe
commit 0f69d61e8e
No known key found for this signature in database
GPG Key ID: 0F16E812E736C24B
7 changed files with 216 additions and 31 deletions

View File

@ -25,6 +25,9 @@ export const mockFsService = {
readDir: vi.fn(),
makeDir: vi.fn(),
deleteDir: vi.fn(),
readJsonFile: vi.fn(),
writeJsonFile: vi.fn(),
writeFile: vi.fn(),
};
export const mockShellService = {

View File

@ -3,6 +3,7 @@ import { spawn, exec } from "child_process";
export class ProcessControl {
constructor(configService, shellService, osService, fsService) {
this.configService = configService;
this.config = configService.get();
this.shell = shellService;
this.os = osService;
@ -18,28 +19,17 @@ export class ProcessControl {
} else {
return await this.shell.run("curl -s https://ip.codex.storage");
}
}
getLogFile = () =>{
// function getCurrentLogFile(config) {
// const timestamp = new Date()
// .toISOString()
// .replaceAll(":", "-")
// .replaceAll(".", "-");
// return path.join(config.logsDir, `codex_${timestamp}.log`);
// }
// todo, maybe use timestamp
return this.fs.pathJoin([this.config.logsDir, "codex.log"]);
}
};
doThing = async () => {
if (this.config.dataDir.length < 1) throw new Error("Missing config: dataDir");
if (this.config.logsDir.length < 1) throw new Error("Missing config: logsDir");
if (this.config.dataDir.length < 1)
throw new Error("Missing config: dataDir");
if (this.config.logsDir.length < 1)
throw new Error("Missing config: logsDir");
console.log("start a codex detached");
console.log("nat: " + await this.getPublicIp());
console.log("nat: " + (await this.getPublicIp()));
console.log("logs dir: " + this.getLogFile());
console.log("data dir: " + this.config.dataDir);
console.log("api port: " + this.config.ports.apiPort);
@ -64,10 +54,15 @@ export class ProcessControl {
console.log("command: " + command);
console.log("\n\n");
var child = spawn(executable, args, { detached: true, stdio: ['ignore', 'ignore', 'ignore']});
this.configService.writeCodexConfigFile();
var child = spawn(executable, args, {
detached: true,
stdio: ["ignore", "ignore", "ignore"],
});
child.unref();
await new Promise((resolve) => setTimeout(resolve, 2000));
return;
}
};
}

View File

@ -99,9 +99,9 @@ export async function main() {
process.on("SIGTERM", handleExit);
process.on("SIGQUIT", handleExit);
const configService = new ConfigService();
const uiService = new UiService();
const fsService = new FsService();
const configService = new ConfigService(fsService);
const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService);
const numberSelector = new NumberSelector(uiService);
const shellService = new ShellService();
@ -133,15 +133,18 @@ export async function main() {
new DataDirMover(fsService, uiService),
);
const processControl = new ProcessControl(configService, shellService, osService, fsService);
const processControl = new ProcessControl(
configService,
shellService,
osService,
fsService,
);
await processControl.doThing();
return;
await mainMenu.show();
return;
try {
while (true) {
console.log("\n" + chalk.cyanBright(ASCII_ART));

View File

@ -1,8 +1,7 @@
import fs from "fs";
import path from "path";
import { getAppDataDir } from "../utils/appData.js";
import {
getAppDataDir,
getCodexBinPath,
getCodexConfigFilePath,
getCodexDataDirDefaultPath,
getCodexLogsDefaultPath,
} from "../utils/appData.js";
@ -11,6 +10,7 @@ const defaultConfig = {
codexExe: "",
// User-selected config options:
codexInstallPath: getCodexBinPath(),
codexConfigFilePath: getCodexConfigFilePath(),
dataDir: getCodexDataDirDefaultPath(),
logsDir: getCodexLogsDefaultPath(),
storageQuota: 8 * 1024 * 1024 * 1024,
@ -22,7 +22,8 @@ const defaultConfig = {
};
export class ConfigService {
constructor() {
constructor(fsService) {
this.fs = fsService;
this.loadConfig();
}
@ -33,11 +34,12 @@ export class ConfigService {
loadConfig = () => {
const filePath = this.getConfigFilename();
try {
if (!fs.existsSync(filePath)) {
if (!this.fs.isFile(filePath)) {
this.config = defaultConfig;
this.saveConfig();
} else {
this.config = this.fs.readJsonFile(filePath);
}
this.config = JSON.parse(fs.readFileSync(filePath));
} catch (error) {
console.error(
`Failed to load config file from '${filePath}' error: '${error}'.`,
@ -49,7 +51,7 @@ export class ConfigService {
saveConfig = () => {
const filePath = this.getConfigFilename();
try {
fs.writeFileSync(filePath, JSON.stringify(this.config));
this.fs.writeJsonFile(filePath, this.config);
} catch (error) {
console.error(
`Failed to save config file to '${filePath}' error: '${error}'.`,
@ -59,6 +61,42 @@ export class ConfigService {
};
getConfigFilename = () => {
return path.join(getAppDataDir(), "config.json");
return this.fs.pathJoin([getAppDataDir(), "config.json"]);
};
getLogFilePath = () => {
// function getCurrentLogFile(config) {
// const timestamp = new Date()
// .toISOString()
// .replaceAll(":", "-")
// .replaceAll(".", "-");
// return path.join(config.logsDir, `codex_${timestamp}.log`);
// }
// todo, maybe use timestamp
return this.fs.pathJoin([this.config.logsDir, "codex.log"]);
};
writeCodexConfigFile = (publicIp, bootstrapNodes) => {
const nl = "\n";
const bootNodes = bootstrapNodes.join(",");
this.fs.writeFile(
this.config.codexConfigFilePath,
`data-dir="${this.format(this.config.dataDir)}"${nl}` +
`log-level=DEBUG${nl}` +
`log-file="${this.format(this.getLogFilePath())}"${nl}` +
`storage-quota=${this.config.storageQuota}${nl}` +
`disc-port=${this.config.ports.discPort}${nl}` +
`listen-addrs=/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}${nl}` +
`api-port=${this.config.ports.apiPort}${nl}` +
`nat=extip:${publicIp}${nl}` +
`api-cors-origin="*"${nl}` +
`bootstrap-node=[${bootNodes}]`,
);
};
format = (str) => {
return str.replaceAll("\\", "/");
};
}

View File

@ -0,0 +1,129 @@
import { describe, beforeEach, it, expect, vi } from "vitest";
import { ConfigService } from "./configService.js";
import { mockFsService } from "../__mocks__/service.mocks.js";
import {
getAppDataDir,
getCodexBinPath,
getCodexConfigFilePath,
getCodexDataDirDefaultPath,
getCodexLogsDefaultPath,
} from "../utils/appData.js";
describe("ConfigService", () => {
const configPath = "/path/to/config.json";
const expectedDefaultConfig = {
codexExe: "",
codexInstallPath: getCodexBinPath(),
codexConfigFilePath: getCodexConfigFilePath(),
dataDir: getCodexDataDirDefaultPath(),
logsDir: getCodexLogsDefaultPath(),
storageQuota: 8 * 1024 * 1024 * 1024,
ports: {
discPort: 8090,
listenPort: 8070,
apiPort: 8080,
},
};
beforeEach(() => {
vi.resetAllMocks();
mockFsService.pathJoin.mockReturnValue(configPath);
});
describe("constructor", () => {
it("formats the config file path", () => {
new ConfigService(mockFsService);
expect(mockFsService.pathJoin).toHaveBeenCalledWith([
getAppDataDir(),
"config.json",
]);
});
it("saves the default config when the config.json file does not exist", () => {
mockFsService.isFile.mockReturnValue(false);
const service = new ConfigService(mockFsService);
expect(mockFsService.isFile).toHaveBeenCalledWith(configPath);
expect(mockFsService.readJsonFile).not.toHaveBeenCalled();
expect(mockFsService.writeJsonFile).toHaveBeenCalledWith(
configPath,
service.config,
);
expect(service.config).toEqual(expectedDefaultConfig);
});
it("loads the config.json file when it does exist", () => {
mockFsService.isFile.mockReturnValue(true);
const savedConfig = {
isTestConfig: "Yes, very",
};
mockFsService.readJsonFile.mockReturnValue(savedConfig);
const service = new ConfigService(mockFsService);
expect(mockFsService.isFile).toHaveBeenCalledWith(configPath);
expect(mockFsService.readJsonFile).toHaveBeenCalledWith(configPath);
expect(mockFsService.writeJsonFile).not.toHaveBeenCalled();
expect(service.config).toEqual(savedConfig);
});
});
describe("getLogFilePath", () => {
it("joins the logsDir with the log filename", () => {
const service = new ConfigService(mockFsService);
const result = "path/to/codex.log";
mockFsService.pathJoin.mockReturnValue(result);
expect(service.getLogFilePath()).toBe(result);
expect(mockFsService.pathJoin).toHaveBeenCalledWith([
expectedDefaultConfig.logsDir,
"codex.log",
]);
});
});
describe("writecodexConfigFile", () => {
const logsPath = "C:\\path\\codex.log";
var configService;
beforeEach(() => {
// use the default config:
mockFsService.isFile.mockReturnValue(false);
configService = new ConfigService(mockFsService);
configService.getLogFilePath = vi.fn();
configService.getLogFilePath.mockReturnValue(logsPath);
});
function formatPath(str) {
return str.replaceAll("\\", "/");
}
it("writes the config file values to the config TOML file", () => {
const publicIp = "1.2.3.4";
const bootstrapNodes = ["boot111", "boot222", "boot333"];
configService.writeCodexConfigFile(publicIp, bootstrapNodes);
const newLine = "\n";
expect(mockFsService.writeFile).toHaveBeenCalledWith(
expectedDefaultConfig.codexConfigFilePath,
`data-dir=\"${formatPath(expectedDefaultConfig.dataDir)}"${newLine}` +
`log-level=DEBUG${newLine}` +
`log-file="${formatPath(logsPath)}"${newLine}` +
`storage-quota=${expectedDefaultConfig.storageQuota}${newLine}` +
`disc-port=${expectedDefaultConfig.ports.discPort}${newLine}` +
`listen-addrs=/ip4/0.0.0.0/tcp/${expectedDefaultConfig.ports.listenPort}${newLine}` +
`api-port=${expectedDefaultConfig.ports.apiPort}${newLine}` +
`nat=extip:${publicIp}${newLine}` +
`api-cors-origin="*"${newLine}` +
`bootstrap-node=[${bootstrapNodes.join(",")}]`,
);
});
});
});

View File

@ -54,4 +54,17 @@ export class FsService {
deleteDir = (dir) => {
fs.rmSync(dir, { recursive: true, force: true });
};
readJsonFile = (filePath) => {
return JSON.parse(fs.readFileSync(filePath));
};
writeJsonFile = (filePath, jsonObject) => {
fs.writeFileSync(filePath, JSON.stringify(jsonObject));
};
writeFile = (filePath, content) => {
console.log("filepath: " + filePath);
fs.writeFileSync(filePath, content);
};
}

View File

@ -13,6 +13,10 @@ export function getCodexBinPath() {
return ensureExists(path.join(appData("codex"), "bin"));
}
export function getCodexConfigFilePath() {
return path.join(appData("codex"), "bin", "config.toml");
}
export function getCodexDataDirDefaultPath() {
// This path does not exist on first startup. That's good: Codex will
// create it with the required access permissions.