Graceful rimraf in updater

This commit is contained in:
Fedor Indutny 2023-03-02 09:57:36 -08:00 committed by GitHub
parent c488b626bf
commit 1e7d658109
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 58 additions and 39 deletions

View file

@ -17,6 +17,7 @@ import {
getTempDir, getTempDir,
deleteTempDir, deleteTempDir,
} from '../../updater/common'; } from '../../updater/common';
import * as log from '../../logging/log';
describe('updater/signatures', () => { describe('updater/signatures', () => {
const windows = parseYaml(`version: 1.23.2 const windows = parseYaml(`version: 1.23.2
@ -164,7 +165,7 @@ releaseDate: '2021-12-03T19:00:23.754Z'
const dir = await createTempDir(); const dir = await createTempDir();
assert.isTrue((await stat(dir)).isDirectory()); assert.isTrue((await stat(dir)).isDirectory());
await deleteTempDir(dir); await deleteTempDir(log, dir);
assert.isFalse(await pathExists(dir), 'Directory should be deleted'); assert.isFalse(await pathExists(dir), 'Directory should be deleted');
}); });
@ -181,7 +182,7 @@ releaseDate: '2021-12-03T19:00:23.754Z'
await mkdir(dir); await mkdir(dir);
await deleteTempDir(dir); await deleteTempDir(log, dir);
assert.isFalse(await pathExists(dir), 'Directory should be deleted'); assert.isFalse(await pathExists(dir), 'Directory should be deleted');
}); });

View file

@ -17,6 +17,7 @@ import {
} from '../../updater/signature'; } from '../../updater/signature';
import { createTempDir, deleteTempDir } from '../../updater/common'; import { createTempDir, deleteTempDir } from '../../updater/common';
import { keyPair } from '../../updater/curve'; import { keyPair } from '../../updater/curve';
import * as log from '../../logging/log';
describe('updater/signatures', () => { describe('updater/signatures', () => {
it('_getFileHash returns correct hash', async () => { it('_getFileHash returns correct hash', async () => {
@ -48,7 +49,7 @@ describe('updater/signatures', () => {
); );
} finally { } finally {
if (tempDir) { if (tempDir) {
await deleteTempDir(tempDir); await deleteTempDir(log, tempDir);
} }
} }
}); });
@ -86,7 +87,7 @@ describe('updater/signatures', () => {
assert.strictEqual(verified, true); assert.strictEqual(verified, true);
} finally { } finally {
if (tempDir) { if (tempDir) {
await deleteTempDir(tempDir); await deleteTempDir(log, tempDir);
} }
} }
}); });
@ -123,7 +124,7 @@ describe('updater/signatures', () => {
assert.strictEqual(verified, false); assert.strictEqual(verified, false);
} finally { } finally {
if (tempDir) { if (tempDir) {
await deleteTempDir(tempDir); await deleteTempDir(log, tempDir);
} }
} }
}); });
@ -160,7 +161,7 @@ describe('updater/signatures', () => {
assert.strictEqual(verified, false); assert.strictEqual(verified, false);
} finally { } finally {
if (tempDir) { if (tempDir) {
await deleteTempDir(tempDir); await deleteTempDir(log, tempDir);
} }
} }
}); });
@ -202,7 +203,7 @@ describe('updater/signatures', () => {
assert.strictEqual(verified, false); assert.strictEqual(verified, false);
} finally { } finally {
if (tempDir) { if (tempDir) {
await deleteTempDir(tempDir); await deleteTempDir(log, tempDir);
} }
} }
}); });
@ -239,7 +240,7 @@ describe('updater/signatures', () => {
assert.strictEqual(verified, false); assert.strictEqual(verified, false);
} finally { } finally {
if (tempDir) { if (tempDir) {
await deleteTempDir(tempDir); await deleteTempDir(log, tempDir);
} }
} }
}); });

View file

@ -18,8 +18,6 @@ import { gt, lt } from 'semver';
import config from 'config'; import config from 'config';
import got from 'got'; import got from 'got';
import { v4 as getGuid } from 'uuid'; import { v4 as getGuid } from 'uuid';
import pify from 'pify';
import rimraf from 'rimraf';
import type { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import { app, ipcMain } from 'electron'; import { app, ipcMain } from 'electron';
@ -41,7 +39,7 @@ import type { SettingsChannel } from '../main/settingsChannel';
import type { LoggerType } from '../types/Logging'; import type { LoggerType } from '../types/Logging';
import { getGotOptions } from './got'; import { getGotOptions } from './got';
import { checkIntegrity, gracefulRename } from './util'; import { checkIntegrity, gracefulRename, gracefulRimraf } from './util';
import type { PrepareDownloadResultType as DifferentialDownloadDataType } from './differential'; import type { PrepareDownloadResultType as DifferentialDownloadDataType } from './differential';
import { import {
prepareDownload as prepareDifferentialDownload, prepareDownload as prepareDifferentialDownload,
@ -50,8 +48,6 @@ import {
isValidPreparedData as isValidDifferentialData, isValidPreparedData as isValidDifferentialData,
} from './differential'; } from './differential';
const rimrafPromise = pify(rimraf);
const INTERVAL = 30 * durations.MINUTE; const INTERVAL = 30 * durations.MINUTE;
type JSONUpdateSchema = { type JSONUpdateSchema = {
@ -604,7 +600,7 @@ export abstract class Updater {
// We could have failed to update differentially due to low free disk // We could have failed to update differentially due to low free disk
// space. Remove all cached updates since we are doing a full download // space. Remove all cached updates since we are doing a full download
// anyway. // anyway.
await rimrafPromise(cacheDir); await gracefulRimraf(this.logger, cacheDir);
cacheDir = await createUpdateCacheDirIfNeeded(); cacheDir = await createUpdateCacheDirIfNeeded();
await this.downloadAndReport( await this.downloadAndReport(
@ -650,7 +646,7 @@ export abstract class Updater {
} }
try { try {
await deleteTempDir(restoreDir); await deleteTempDir(this.logger, restoreDir);
} catch (error) { } catch (error) {
this.logger.warn( this.logger.warn(
'downloadUpdate: Failed to remove backup folder, ignoring', 'downloadUpdate: Failed to remove backup folder, ignoring',
@ -661,7 +657,7 @@ export abstract class Updater {
return { updateFilePath: targetUpdatePath, signature }; return { updateFilePath: targetUpdatePath, signature };
} finally { } finally {
if (!tempPathFailover) { if (!tempPathFailover) {
await deleteTempDir(tempDir); await deleteTempDir(this.logger, tempDir);
} }
} }
} }
@ -906,7 +902,10 @@ export async function createUpdateCacheDirIfNeeded(): Promise<string> {
return targetDir; return targetDir;
} }
export async function deleteTempDir(targetDir: string): Promise<void> { export async function deleteTempDir(
logger: LoggerType,
targetDir: string
): Promise<void> {
if (await pathExists(targetDir)) { if (await pathExists(targetDir)) {
const pathInfo = await stat(targetDir); const pathInfo = await stat(targetDir);
if (!pathInfo.isDirectory()) { if (!pathInfo.isDirectory()) {
@ -923,7 +922,7 @@ export async function deleteTempDir(targetDir: string): Promise<void> {
); );
} }
await rimrafPromise(targetDir); await gracefulRimraf(logger, targetDir);
} }
export function getCliOptions<T>(options: ParserConfiguration['options']): T { export function getCliOptions<T>(options: ParserConfiguration['options']): T {

View file

@ -5,6 +5,8 @@ import { createReadStream } from 'fs';
import { rename } from 'fs/promises'; import { rename } from 'fs/promises';
import { pipeline } from 'stream/promises'; import { pipeline } from 'stream/promises';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import rimraf from 'rimraf';
import { promisify } from 'util';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import type { LoggerType } from '../types/Logging'; import type { LoggerType } from '../types/Logging';
@ -12,6 +14,8 @@ import * as durations from '../util/durations';
import { isOlderThan } from '../util/timestamp'; import { isOlderThan } from '../util/timestamp';
import { sleep } from '../util/sleep'; import { sleep } from '../util/sleep';
const rimrafPromise = promisify(rimraf);
export type CheckIntegrityResultType = Readonly< export type CheckIntegrityResultType = Readonly<
| { | {
ok: true; ok: true;
@ -48,30 +52,32 @@ export async function checkIntegrity(
} }
} }
async function doGracefulRename({ async function doGracefulFSOperation<Args extends ReadonlyArray<unknown>>({
name,
operation,
args,
logger, logger,
fromPath,
toPath,
startedAt, startedAt,
retryCount, retryCount,
retryAfter = 5 * durations.SECOND, retryAfter = 5 * durations.SECOND,
timeout = 5 * durations.MINUTE, timeout = 5 * durations.MINUTE,
}: { }: {
name: string;
operation: (...args: Args) => Promise<void>;
args: Args;
logger: LoggerType; logger: LoggerType;
fromPath: string;
toPath: string;
startedAt: number; startedAt: number;
retryCount: number; retryCount: number;
retryAfter?: number; retryAfter?: number;
timeout?: number; timeout?: number;
}): Promise<void> { }): Promise<void> {
const logId = `gracefulFS(${name})`;
try { try {
await rename(fromPath, toPath); await operation(...args);
if (retryCount !== 0) { if (retryCount !== 0) {
logger.info( logger.info(
`gracefulRename: succeeded after ${retryCount} retries, renamed ` + `${logId}: succeeded after ${retryCount} retries, ${args.join(', ')}`
`${fromPath} to ${toPath}`
); );
} }
} catch (error) { } catch (error) {
@ -80,25 +86,22 @@ async function doGracefulRename({
} }
if (isOlderThan(startedAt, timeout)) { if (isOlderThan(startedAt, timeout)) {
logger.warn( logger.warn(`${logId}: timed out, ${args.join(', ')}`);
'gracefulRename: timed out while retrying renaming ' +
`${fromPath} to ${toPath}`
);
throw error; throw error;
} }
logger.warn( logger.warn(
`gracefulRename: got ${error.code} when renaming ` + `${logId}: got ${error.code} when running on ${args.join(', ')}; ` +
`${fromPath} to ${toPath}, retrying in one second. ` + `retrying in one second. (retryCount=${retryCount})`
`(retryCount=${retryCount})`
); );
await sleep(retryAfter); await sleep(retryAfter);
return doGracefulRename({ return doGracefulFSOperation({
name,
operation,
args,
logger, logger,
fromPath,
toPath,
startedAt, startedAt,
retryCount: retryCount + 1, retryCount: retryCount + 1,
retryAfter, retryAfter,
@ -112,10 +115,25 @@ export async function gracefulRename(
fromPath: string, fromPath: string,
toPath: string toPath: string
): Promise<void> { ): Promise<void> {
return doGracefulRename({ return doGracefulFSOperation({
name: 'rename',
operation: rename,
args: [fromPath, toPath],
logger,
startedAt: Date.now(),
retryCount: 0,
});
}
export async function gracefulRimraf(
logger: LoggerType,
path: string
): Promise<void> {
return doGracefulFSOperation({
name: 'rimraf',
operation: rimrafPromise,
args: [path],
logger, logger,
fromPath,
toPath,
startedAt: Date.now(), startedAt: Date.now(),
retryCount: 0, retryCount: 0,
}); });