UNPKG

@atomist/automation-client

Version:

Atomist API for software low-level client

136 lines (119 loc) 5.15 kB
import * as os from "os"; import * as path from "path"; import { increment } from "../../internal/util/metric"; import { logger } from "../../util/logger"; import { CloneDirectoryInfo, CloneOptions, DirectoryManager, } from "./DirectoryManager"; import { StableDirectoryManager } from "./StableDirectoryManager"; import { TmpDirectoryManager } from "./tmpDirectoryManager"; const AtomistWorkingDirectory = path.join(".atomist", "cache"); const AbsoluteAtomistWorkingDirectory = path.join(os.homedir(), AtomistWorkingDirectory); const cache = new StableDirectoryManager({ reuseDirectories: true, baseDir: AbsoluteAtomistWorkingDirectory, cleanOnExit: false, }); /** * Designed to accommodate occasional writes to the same repositories, * this keeps one clone available for each repository. Every time that repository is requested, * if that clone is available, we return it. (The caller gets to fetch, clean, etc. The directory could be dirty.) * If that clone is locked by some other automation invocation, this * DirectoryManager returns a temporary directory, and you get to clone into that. * * If the returned CloneDirectoryInfo has type: "empty-directory" * then the caller should clone into it (not from it, you're not in the parent directory). * If it has type: "existing-directory" then fetch, clean, checkout etc. given it's already cloned. * * @type {{directoryFor: * ((owner: string, repo: string, branch: string, opts: CloneOptions) => Promise<CloneDirectoryInfo>)}} */ export const CachingDirectoryManager: DirectoryManager = { directoryFor(owner: string, repo: string, branch: string, opts: CloneOptions): Promise<CloneDirectoryInfo> { return cache.directoryFor(owner, repo, branch, opts).then(existing => pleaseLock(existing.path).then(lockResult => { if (lockResult.success) { incrementReuse(owner, repo); return { ...existing, release: () => { logger.debug("Releasing lock on '%s'", existing.path); return lockResult.release().then(existing.release); }, invalidate: () => { logger.debug("Invalidating '%s'", existing.path); return cache.invalidate(existing) .then(() => { logger.debug("Invalidated. Now releasing lock"); return lockResult.release().then(existing.release); }); }, provenance: (existing.provenance || "") + " successfully locked", }; } else { logger.debug("Lock detected on '%s'", existing.path); incrementFallback(owner, repo); return TmpDirectoryManager.directoryFor(owner, repo, branch, opts).then(cdi => ({ ...cdi, provenance: `Tried '${existing.path}' but it was locked. ` + (cdi.provenance || ""), })); } })); }, }; export const ReuseKey = "directory_cache.reuse"; export const FallbackKey = "directory_cache.fallback"; function incrementReuse(owner: string, repo: string): void { increment(`${ReuseKey}.${keyFor(owner, repo)}`); increment(ReuseKey); } function incrementFallback(owner: string, repo: string): void { increment(`${FallbackKey}.${keyFor(owner, repo)}`); increment(FallbackKey); } function keyFor(owner: string, repo: string): string { return `${owner}/${repo}`; } /* * file locking. only used here */ import lockfile = require("proper-lockfile"); interface LockAcquired { success: true; release: () => Promise<void>; } interface NoLockForYou { success: false; error: Error; } // for testing export { pleaseLock, LockResult, LockAcquired, NoLockForYou }; type LockResult = LockAcquired | NoLockForYou; function pleaseLock(lockPath: string): Promise<LockResult> { return new Promise<LockResult>((resolve, reject) => { lockfile.lock(lockPath, (error, releaseCallback) => { if (error) { if (error.code === "ELOCKED") { resolve({ success: false, error }); } reject(error); } else { // make the release function return a promise too. Its callback accepts a possible error. const release = () => new Promise<void>((releaseResolve, releaseReject) => { releaseCallback(err => { if (err) { releaseReject(err); } else { releaseResolve(); } }); }); resolve({ success: true, release }); } }); }); }