Compare commits

..

5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 855f591992 Improve type safety for fileHandle and release variables
- Add explicit FileHandle type annotation for fileHandle variable
- Change fileHandle check from truthy to explicit undefined check
- Change release type from null to undefined for better type safety

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:49:27 +00:00
copilot-swe-agent[bot] d30eb2008c Add error handling for file handle close operation
- Wrap fileHandle.close() in try-catch to prevent masking original errors
- Log close errors without throwing to preserve error context

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:46:51 +00:00
copilot-swe-agent[bot] e1ce0f3fa9 Improve error handling based on code review feedback
- Use finally block to ensure file handle is closed even on error
- Extract error messages properly using instanceof Error check
- Improve error message formatting for better debugging

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:45:34 +00:00
copilot-swe-agent[bot] 82514e65e1 Add server timeout configuration and improve error handling for script save operations
- Configure HTTP server timeouts (requestTimeout: 5min, headersTimeout: 2min, keepAliveTimeout: 65s)
- Add better error logging in PUT /scripts endpoint
- Improve writeFileWithLock error handling with descriptive messages and proper cleanup
- Ensure lock is always released even on error

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:43:11 +00:00
copilot-swe-agent[bot] 31261190f0 Initial plan 2025-12-27 08:35:20 +00:00
5 changed files with 91 additions and 186 deletions
+2
View File
@@ -206,6 +206,7 @@ export default (app: Router) => {
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, content, path } = req.body as {
filename: string;
@@ -223,6 +224,7 @@ export default (app: Router) => {
await writeFileWithLock(filePath, content);
return res.send({ code: 200 });
} catch (e) {
logger.error('🔥 error saving script: %o', e);
return next(e);
}
},
-160
View File
@@ -10,7 +10,6 @@ import Logger from '../loaders/logger';
import { writeFileWithLock } from '../shared/utils';
import { DependenceTypes } from '../data/dependence';
import { FormData } from 'undici';
import os from 'os';
export * from './share';
@@ -591,162 +590,3 @@ export function getUninstallCommand(
export function isDemoEnv() {
return process.env.DeployEnv === 'demo';
}
// OS detection for Linux mirror configuration
let osType: 'Debian' | 'Ubuntu' | 'Alpine' | undefined;
async function getOSReleaseInfo(): Promise<string> {
try {
const osRelease = await fs.readFile('/etc/os-release', 'utf8');
return osRelease;
} catch (error) {
Logger.error(`Failed to read /etc/os-release: ${error}`);
return '';
}
}
function isDebian(osReleaseInfo: string): boolean {
return osReleaseInfo.includes('Debian');
}
function isUbuntu(osReleaseInfo: string): boolean {
return osReleaseInfo.includes('Ubuntu');
}
function isAlpine(osReleaseInfo: string): boolean {
return osReleaseInfo.includes('Alpine');
}
export async function detectOS(): Promise<
'Debian' | 'Ubuntu' | 'Alpine' | undefined
> {
if (osType) return osType;
const platform = os.platform();
if (platform === 'linux') {
const osReleaseInfo = await getOSReleaseInfo();
// Check Ubuntu before Debian since Ubuntu is based on Debian
if (isUbuntu(osReleaseInfo)) {
osType = 'Ubuntu';
} else if (isDebian(osReleaseInfo)) {
osType = 'Debian';
} else if (isAlpine(osReleaseInfo)) {
osType = 'Alpine';
} else {
Logger.error(`Unknown Linux Distribution: ${osReleaseInfo}`);
console.error(`Unknown Linux Distribution: ${osReleaseInfo}`);
}
} else if (platform === 'darwin') {
osType = undefined;
} else {
Logger.error(`Unsupported platform: ${platform}`);
console.error(`Unsupported platform: ${platform}`);
}
return osType;
}
async function getCurrentMirrorDomain(
filePath: string,
): Promise<string | null> {
try {
const fileContent = await fs.readFile(filePath, 'utf8');
const lines = fileContent.split('\n');
for (const line of lines) {
if (line.trim().startsWith('#')) {
continue;
}
const match = line.match(/https?:\/\/[^\/]+/);
if (match) {
return match[0];
}
}
return null;
} catch (error) {
Logger.error(`Failed to read mirror configuration file ${filePath}: ${error}`);
return null;
}
}
function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function replaceDomainInFile(
filePath: string,
oldDomainWithScheme: string,
newDomainWithScheme: string,
): Promise<void> {
// Ensure the new domain has a trailing slash before replacement
if (!newDomainWithScheme.endsWith('/')) {
newDomainWithScheme += '/';
}
let fileContent = await fs.readFile(filePath, 'utf8');
// Escape special regex characters in the old domain
const escapedOldDomain = escapeRegExp(oldDomainWithScheme);
let updatedContent = fileContent.replace(
new RegExp(escapedOldDomain, 'g'),
newDomainWithScheme,
);
await writeFileWithLock(filePath, updatedContent);
}
async function _updateLinuxMirror(
osType: string,
mirrorDomainWithScheme: string,
): Promise<string> {
let filePath: string, currentDomainWithScheme: string | null;
switch (osType) {
case 'Debian':
filePath = '/etc/apt/sources.list.d/debian.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://deb.debian.org',
);
return 'apt-get update';
} else {
throw Error(`Current mirror domain not found.`);
}
case 'Ubuntu':
filePath = '/etc/apt/sources.list.d/ubuntu.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://archive.ubuntu.com',
);
return 'apt-get update';
} else {
throw Error(`Current mirror domain not found.`);
}
case 'Alpine':
filePath = '/etc/apk/repositories';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org',
);
return 'apk update';
} else {
throw Error(`Current mirror domain not found.`);
}
default:
throw Error('Unsupported OS type for updating mirrors.');
}
}
export async function updateLinuxMirrorFile(mirror: string): Promise<string> {
const detectedOS = await detectOS();
if (!detectedOS) {
throw Error(`Unknown Linux Distribution`);
}
return await _updateLinuxMirror(detectedOS, mirror);
}
+11
View File
@@ -16,6 +16,17 @@ export class HttpServerService {
metricsService.record('http_service_start', 1, {
port: port.toString(),
});
// Set server timeouts to prevent premature connection drops
if (this.server) {
// Timeout for receiving the entire request (including body) - 5 minutes
this.server.requestTimeout = 300000;
// Timeout for headers - 2 minutes
this.server.headersTimeout = 120000;
// Keep-alive timeout - 65 seconds (slightly more than typical load balancer timeout)
this.server.keepAliveTimeout = 65000;
}
resolve(this.server);
});
+24 -10
View File
@@ -17,7 +17,6 @@ import {
readDirs,
rmPath,
setSystemTimezone,
updateLinuxMirrorFile,
} from '../config/util';
import {
DependenceModel,
@@ -215,11 +214,33 @@ export default class SystemService {
onEnd?: () => void,
) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
let defaultDomain = 'https://dl-cdn.alpinelinux.org';
let targetDomain = 'https://dl-cdn.alpinelinux.org';
if (os.platform() !== 'linux') {
return;
}
const command = await updateLinuxMirrorFile(info.linuxMirror || '');
let hasError = false;
const content = await fs.promises.readFile('/etc/apk/repositories', {
encoding: 'utf-8',
});
const domainMatch = content.match(/(http.*)\/alpine\/.*/);
if (domainMatch) {
defaultDomain = domainMatch[1];
}
if (info.linuxMirror) {
targetDomain = info.linuxMirror;
}
const command = `sed -i 's/${defaultDomain.replace(
/\//g,
'\\/',
)}/${targetDomain.replace(
/\//g,
'\\/',
)}/g' /etc/apk/repositories && apk update -f`;
this.scheduleService.runTask(
command,
{
@@ -233,15 +254,8 @@ export default class SystemService {
message: 'update linux mirror end',
});
onEnd?.();
if (!hasError) {
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
}
},
onError: async (message: string) => {
hasError = true;
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
},
onLog: async (message: string) => {
+54 -16
View File
@@ -1,8 +1,9 @@
import { lock } from 'proper-lockfile';
import os from 'os';
import path from 'path';
import { writeFile, open, chmod } from 'fs/promises';
import { writeFile, open, chmod, FileHandle } from 'fs/promises';
import { fileExist } from '../config/util';
import Logger from '../loaders/logger';
function getUniqueLockPath(filePath: string) {
const sanitizedPath = filePath
@@ -19,24 +20,61 @@ export async function writeFileWithLock(
if (typeof options === 'string') {
options = { encoding: options };
}
// Ensure file exists before locking
if (!(await fileExist(filePath))) {
const fileHandle = await open(filePath, 'w');
fileHandle.close();
let fileHandle: FileHandle | undefined;
try {
fileHandle = await open(filePath, 'w');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create file ${filePath}: ${errorMessage}`);
} finally {
if (fileHandle !== undefined) {
try {
await fileHandle.close();
} catch (closeError) {
// Log close error but don't throw to avoid masking the original error
Logger.error(`Failed to close file handle for ${filePath}:`, closeError);
}
}
}
}
const lockfilePath = getUniqueLockPath(filePath);
let release: (() => Promise<void>) | undefined;
const release = await lock(filePath, {
retries: {
retries: 10,
factor: 2,
minTimeout: 100,
maxTimeout: 3000,
},
lockfilePath,
});
await writeFile(filePath, content, { encoding: 'utf8', ...options });
if (options?.mode) {
await chmod(filePath, options.mode);
try {
release = await lock(filePath, {
retries: {
retries: 10,
factor: 2,
minTimeout: 100,
maxTimeout: 3000,
},
lockfilePath,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to acquire lock for ${filePath}: ${errorMessage}`);
}
try {
await writeFile(filePath, content, { encoding: 'utf8', ...options });
if (options?.mode) {
await chmod(filePath, options.mode);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to write to file ${filePath}: ${errorMessage}`);
} finally {
if (release) {
try {
await release();
} catch (error) {
// Log but don't throw on release failure
Logger.error(`Failed to release lock for ${filePath}:`, error);
}
}
}
await release();
}