Merge branch 'justlovemaki:main' into main
This commit is contained in:
commit
6bfa3c5bfc
10 changed files with 210 additions and 17 deletions
|
|
@ -585,6 +585,9 @@ AIClient-2-APIプロジェクトに貢献してくれたすべての開発者に
|
||||||
- [**醉春風**](https://github.com/handsometong "28.8")
|
- [**醉春風**](https://github.com/handsometong "28.8")
|
||||||
- [**crazy**](https://github.com/404 "88")
|
- [**crazy**](https://github.com/404 "88")
|
||||||
- [**清宵落了灯花**](https://github.com/Lanternmorning "16")
|
- [**清宵落了灯花**](https://github.com/Lanternmorning "16")
|
||||||
|
- [**郭铁**](https://github.com/guotie "20")
|
||||||
|
- [**落叶聚名**](https://github.com/mb5u88-debug "88")
|
||||||
|
- [**匿名**](https://github.com/404 "8.88")
|
||||||
|
|
||||||
### 🌟 Star History
|
### 🌟 Star History
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -582,6 +582,9 @@ kill -9 <PID>
|
||||||
- [**醉春风**](https://github.com/handsometong "28.8")
|
- [**醉春风**](https://github.com/handsometong "28.8")
|
||||||
- [**crazy**](https://github.com/404 "88")
|
- [**crazy**](https://github.com/404 "88")
|
||||||
- [**清宵落了灯花**](https://github.com/Lanternmorning "16")
|
- [**清宵落了灯花**](https://github.com/Lanternmorning "16")
|
||||||
|
- [**郭铁**](https://github.com/guotie "20")
|
||||||
|
- [**落叶聚名**](https://github.com/mb5u88-debug "88")
|
||||||
|
- [**匿名**](https://github.com/404 "8.88")
|
||||||
|
|
||||||
### 🌟 Star History
|
### 🌟 Star History
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -585,6 +585,9 @@ We are grateful for the support from our sponsors:
|
||||||
- [**醉春风**](https://github.com/handsometong "28.8")
|
- [**醉春风**](https://github.com/handsometong "28.8")
|
||||||
- [**crazy**](https://github.com/404 "88")
|
- [**crazy**](https://github.com/404 "88")
|
||||||
- [**清宵落了灯花**](https://github.com/Lanternmorning "16")
|
- [**清宵落了灯花**](https://github.com/Lanternmorning "16")
|
||||||
|
- [**郭铁**](https://github.com/guotie "20")
|
||||||
|
- [**落叶聚名**](https://github.com/mb5u88-debug "88")
|
||||||
|
- [**匿名**](https://github.com/404 "8.88")
|
||||||
|
|
||||||
### 🌟 Star History
|
### 🌟 Star History
|
||||||
|
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
2.6.8
|
2.6.9
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { countTokens } from '@anthropic-ai/tokenizer';
|
||||||
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
|
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
|
||||||
import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js';
|
import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js';
|
||||||
import { getProviderPoolManager } from '../../services/service-manager.js';
|
import { getProviderPoolManager } from '../../services/service-manager.js';
|
||||||
|
import { acquireFileLock } from '../../utils/file-lock.js';
|
||||||
|
|
||||||
const KIRO_THINKING = {
|
const KIRO_THINKING = {
|
||||||
MAX_BUDGET_TOKENS: 24576,
|
MAX_BUDGET_TOKENS: 24576,
|
||||||
|
|
@ -426,8 +427,10 @@ async initializeAuth(forceRefresh = false) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to save credentials to a file
|
// Helper to save credentials to a file (with file locking to prevent concurrent write corruption)
|
||||||
const saveCredentialsToFile = async (filePath, newData) => {
|
const saveCredentialsToFile = async (filePath, newData) => {
|
||||||
|
// 获取文件锁,防止并发写入
|
||||||
|
const releaseLock = await acquireFileLock(filePath);
|
||||||
try {
|
try {
|
||||||
let existingData = {};
|
let existingData = {};
|
||||||
try {
|
try {
|
||||||
|
|
@ -445,6 +448,9 @@ async initializeAuth(forceRefresh = false) {
|
||||||
console.info(`[Kiro Auth] Updated token file: ${filePath}`);
|
console.info(`[Kiro Auth] Updated token file: ${filePath}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Kiro Auth] Failed to write token to file ${filePath}: ${error.message}`);
|
console.error(`[Kiro Auth] Failed to write token to file ${filePath}: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
// 确保锁被释放
|
||||||
|
releaseLock();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { getProviderModels } from '../provider-models.js';
|
||||||
import { handleGeminiAntigravityOAuth } from '../../auth/oauth-handlers.js';
|
import { handleGeminiAntigravityOAuth } from '../../auth/oauth-handlers.js';
|
||||||
import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils/proxy-utils.js';
|
import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils/proxy-utils.js';
|
||||||
import { cleanJsonSchemaProperties } from '../../converters/utils.js';
|
import { cleanJsonSchemaProperties } from '../../converters/utils.js';
|
||||||
|
import { acquireFileLock } from '../../utils/file-lock.js';
|
||||||
|
|
||||||
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
|
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
|
||||||
const httpAgent = new http.Agent({
|
const httpAgent = new http.Agent({
|
||||||
|
|
@ -797,8 +798,8 @@ export class AntigravityApiService {
|
||||||
console.log('[Antigravity Auth] Token expiring soon or force refresh requested. Refreshing token...');
|
console.log('[Antigravity Auth] Token expiring soon or force refresh requested. Refreshing token...');
|
||||||
const { credentials: newCredentials } = await this.authClient.refreshAccessToken();
|
const { credentials: newCredentials } = await this.authClient.refreshAccessToken();
|
||||||
this.authClient.setCredentials(newCredentials);
|
this.authClient.setCredentials(newCredentials);
|
||||||
// 保存刷新后的凭证到文件
|
// 保存刷新后的凭证到文件(使用文件锁)
|
||||||
await fs.writeFile(credPath, JSON.stringify(newCredentials, null, 2));
|
await this._saveCredentialsToFile(credPath, newCredentials);
|
||||||
console.log(`[Antigravity Auth] Token refreshed and saved to ${credPath} successfully.`);
|
console.log(`[Antigravity Auth] Token refreshed and saved to ${credPath} successfully.`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -874,6 +875,23 @@ export class AntigravityApiService {
|
||||||
return expiryTime <= (currentTime + refreshSkewMs);
|
return expiryTime <= (currentTime + refreshSkewMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存凭证到文件(使用文件锁防止并发写入)
|
||||||
|
* @param {string} filePath - 凭证文件路径
|
||||||
|
* @param {Object} credentials - 凭证数据
|
||||||
|
*/
|
||||||
|
async _saveCredentialsToFile(filePath, credentials) {
|
||||||
|
const releaseLock = await acquireFileLock(filePath);
|
||||||
|
try {
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(credentials, null, 2));
|
||||||
|
console.log(`[Antigravity Auth] Credentials saved to ${filePath}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Antigravity Auth] Failed to save credentials to ${filePath}: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
releaseLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async discoverProjectAndModels() {
|
async discoverProjectAndModels() {
|
||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
console.log(`[Antigravity] Using pre-configured Project ID: ${this.projectId}`);
|
console.log(`[Antigravity] Using pre-configured Project ID: ${this.projectId}`);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { API_ACTIONS, formatExpiryTime, isRetryableNetworkError } from '../../ut
|
||||||
import { getProviderModels } from '../provider-models.js';
|
import { getProviderModels } from '../provider-models.js';
|
||||||
import { handleGeminiCliOAuth } from '../../auth/oauth-handlers.js';
|
import { handleGeminiCliOAuth } from '../../auth/oauth-handlers.js';
|
||||||
import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils/proxy-utils.js';
|
import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils/proxy-utils.js';
|
||||||
|
import { acquireFileLock } from '../../utils/file-lock.js';
|
||||||
|
|
||||||
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
|
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
|
||||||
const httpAgent = new http.Agent({
|
const httpAgent = new http.Agent({
|
||||||
|
|
@ -274,8 +275,8 @@ export class GeminiApiService {
|
||||||
console.log('[Gemini Auth] Forcing token refresh...');
|
console.log('[Gemini Auth] Forcing token refresh...');
|
||||||
const { credentials: newCredentials } = await this.authClient.refreshAccessToken();
|
const { credentials: newCredentials } = await this.authClient.refreshAccessToken();
|
||||||
this.authClient.setCredentials(newCredentials);
|
this.authClient.setCredentials(newCredentials);
|
||||||
// Save refreshed credentials back to file
|
// Save refreshed credentials back to file (with file locking)
|
||||||
await fs.writeFile(credPath, JSON.stringify(newCredentials, null, 2));
|
await this._saveCredentialsToFile(credPath, newCredentials);
|
||||||
console.log('[Gemini Auth] Token refreshed and saved successfully.');
|
console.log('[Gemini Auth] Token refreshed and saved successfully.');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -621,6 +622,23 @@ export class GeminiApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存凭证到文件(使用文件锁防止并发写入)
|
||||||
|
* @param {string} filePath - 凭证文件路径
|
||||||
|
* @param {Object} credentials - 凭证数据
|
||||||
|
*/
|
||||||
|
async _saveCredentialsToFile(filePath, credentials) {
|
||||||
|
const releaseLock = await acquireFileLock(filePath);
|
||||||
|
try {
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(credentials, null, 2));
|
||||||
|
console.log(`[Gemini Auth] Credentials saved to ${filePath}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Gemini Auth] Failed to save credentials to ${filePath}: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
releaseLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取模型配额信息
|
* 获取模型配额信息
|
||||||
* @returns {Promise<Object>} 模型配额信息
|
* @returns {Promise<Object>} 模型配额信息
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
|
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
|
||||||
import { isRetryableNetworkError } from '../../utils/common.js';
|
import { isRetryableNetworkError } from '../../utils/common.js';
|
||||||
|
import { acquireFileLock } from '../../utils/file-lock.js';
|
||||||
|
|
||||||
// iFlow API 端点
|
// iFlow API 端点
|
||||||
const IFLOW_API_BASE_URL = 'https://apis.iflow.cn/v1';
|
const IFLOW_API_BASE_URL = 'https://apis.iflow.cn/v1';
|
||||||
|
|
@ -132,11 +133,13 @@ async function loadTokenFromFile(filePath) {
|
||||||
* @param {IFlowTokenStorage} tokenStorage - Token 存储对象
|
* @param {IFlowTokenStorage} tokenStorage - Token 存储对象
|
||||||
*/
|
*/
|
||||||
async function saveTokenToFile(filePath, tokenStorage) {
|
async function saveTokenToFile(filePath, tokenStorage) {
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.join(process.cwd(), filePath);
|
||||||
|
|
||||||
|
// 获取文件锁,防止并发写入
|
||||||
|
const releaseLock = await acquireFileLock(absolutePath);
|
||||||
try {
|
try {
|
||||||
const absolutePath = path.isAbsolute(filePath)
|
|
||||||
? filePath
|
|
||||||
: path.join(process.cwd(), filePath);
|
|
||||||
|
|
||||||
// 确保目录存在
|
// 确保目录存在
|
||||||
const dir = path.dirname(absolutePath);
|
const dir = path.dirname(absolutePath);
|
||||||
await fs.mkdir(dir, { recursive: true });
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
|
@ -157,6 +160,9 @@ async function saveTokenToFile(filePath, tokenStorage) {
|
||||||
console.log(`[iFlow] Token saved to: ${filePath} (refresh_token: ${json.refresh_token ? json.refresh_token.substring(0, 8) + '...' : 'EMPTY'})`);
|
console.log(`[iFlow] Token saved to: ${filePath} (refresh_token: ${json.refresh_token ? json.refresh_token.substring(0, 8) + '...' : 'EMPTY'})`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`[iFlow] Failed to save token to file: ${error.message}`);
|
throw new Error(`[iFlow] Failed to save token to file: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
// 确保锁被释放
|
||||||
|
releaseLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { getProviderModels } from '../provider-models.js';
|
||||||
import { handleQwenOAuth } from '../../auth/oauth-handlers.js';
|
import { handleQwenOAuth } from '../../auth/oauth-handlers.js';
|
||||||
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
|
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
|
||||||
import { isRetryableNetworkError } from '../../utils/common.js';
|
import { isRetryableNetworkError } from '../../utils/common.js';
|
||||||
|
import { acquireFileLock } from '../../utils/file-lock.js';
|
||||||
|
|
||||||
// --- Constants ---
|
// --- Constants ---
|
||||||
const QWEN_DIR = '.qwen';
|
const QWEN_DIR = '.qwen';
|
||||||
|
|
@ -388,9 +389,18 @@ export class QwenApiService {
|
||||||
|
|
||||||
async _cacheQwenCredentials(credentials) {
|
async _cacheQwenCredentials(credentials) {
|
||||||
const filePath = this._getQwenCachedCredentialPath();
|
const filePath = this._getQwenCachedCredentialPath();
|
||||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
// 获取文件锁,防止并发写入
|
||||||
const credString = JSON.stringify(credentials, null, 2);
|
const releaseLock = await acquireFileLock(filePath);
|
||||||
await fs.writeFile(filePath, credString);
|
try {
|
||||||
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||||
|
const credString = JSON.stringify(credentials, null, 2);
|
||||||
|
await fs.writeFile(filePath, credString);
|
||||||
|
console.log(`[Qwen Auth] Credentials cached to ${filePath}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Qwen Auth] Failed to cache credentials to ${filePath}: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
releaseLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentEndpoint(resourceUrl) {
|
getCurrentEndpoint(resourceUrl) {
|
||||||
|
|
@ -841,10 +851,17 @@ class SharedTokenManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveCredentialsToFile(context, credentials) {
|
async saveCredentialsToFile(context, credentials) {
|
||||||
await fs.mkdir(path.dirname(context.credentialFilePath), { recursive: true, mode: 0o700 });
|
// 获取文件锁,防止并发写入
|
||||||
await fs.writeFile(context.credentialFilePath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
|
const releaseLock = await acquireFileLock(context.credentialFilePath);
|
||||||
const stats = await fs.stat(context.credentialFilePath);
|
try {
|
||||||
context.memoryCache.fileModTime = stats.mtimeMs;
|
await fs.mkdir(path.dirname(context.credentialFilePath), { recursive: true, mode: 0o700 });
|
||||||
|
await fs.writeFile(context.credentialFilePath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
|
||||||
|
const stats = await fs.stat(context.credentialFilePath);
|
||||||
|
context.memoryCache.fileModTime = stats.mtimeMs;
|
||||||
|
} finally {
|
||||||
|
// 确保锁被释放
|
||||||
|
releaseLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isTokenValid(credentials) {
|
isTokenValid(credentials) {
|
||||||
|
|
|
||||||
119
src/utils/file-lock.js
Normal file
119
src/utils/file-lock.js
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件锁管理器 - 防止并发写入导致文件损坏
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 多个异步操作同时读写同一文件
|
||||||
|
* - 防止读-改-写竞争条件(Race Condition)
|
||||||
|
* - 防止写入交错导致文件内容损坏
|
||||||
|
*
|
||||||
|
* 注意:这是进程内锁,只能防止同一 Node.js 进程内的并发。
|
||||||
|
* 如果需要跨进程文件锁,请使用 proper-lockfile 等库。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 存储每个文件的锁队列(Promise 链)
|
||||||
|
// 每个文件对应一个 Promise,新的锁请求会链接到当前 Promise 之后
|
||||||
|
const fileLockQueues = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件锁,确保同一时间只有一个操作可以访问特定文件
|
||||||
|
*
|
||||||
|
* 实现原理:使用 Promise 链实现队列机制
|
||||||
|
* - 每个文件维护一个 Promise 链
|
||||||
|
* - 新的锁请求会等待当前链完成,然后创建新的链节点
|
||||||
|
* - 这确保了锁的获取是严格串行的,避免竞态条件
|
||||||
|
*
|
||||||
|
* @param {string} filePath - 文件路径
|
||||||
|
* @returns {Promise<() => void>} 释放锁的函数
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const releaseLock = await acquireFileLock('/path/to/file.json');
|
||||||
|
* try {
|
||||||
|
* // 读取、修改、写入文件
|
||||||
|
* const data = await fs.readFile(filePath, 'utf8');
|
||||||
|
* const modified = JSON.parse(data);
|
||||||
|
* modified.key = 'new value';
|
||||||
|
* await fs.writeFile(filePath, JSON.stringify(modified, null, 2));
|
||||||
|
* } finally {
|
||||||
|
* releaseLock(); // 确保锁被释放
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export async function acquireFileLock(filePath) {
|
||||||
|
const normalizedPath = path.resolve(filePath);
|
||||||
|
|
||||||
|
// 获取当前队列中的最后一个 Promise(如果存在)
|
||||||
|
const currentLock = fileLockQueues.get(normalizedPath) || Promise.resolve();
|
||||||
|
|
||||||
|
// 创建释放锁的 resolver
|
||||||
|
let releaseLock;
|
||||||
|
const newLockPromise = new Promise(resolve => {
|
||||||
|
releaseLock = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 立即将新的 Promise 加入队列(在 await 之前!)
|
||||||
|
// 这是关键:确保后续请求会等待这个新的 Promise
|
||||||
|
fileLockQueues.set(normalizedPath, newLockPromise);
|
||||||
|
|
||||||
|
// 等待前一个锁释放
|
||||||
|
await currentLock;
|
||||||
|
|
||||||
|
// 返回释放锁的函数
|
||||||
|
return () => {
|
||||||
|
// 只有当当前锁仍是队列中的最后一个时才清理
|
||||||
|
// 否则保留队列让后续请求继续等待
|
||||||
|
if (fileLockQueues.get(normalizedPath) === newLockPromise) {
|
||||||
|
fileLockQueues.delete(normalizedPath);
|
||||||
|
}
|
||||||
|
releaseLock();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用文件锁执行操作的便捷函数
|
||||||
|
* @param {string} filePath - 文件路径
|
||||||
|
* @param {Function} operation - 要执行的异步操作
|
||||||
|
* @returns {Promise<any>} 操作的返回值
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const result = await withFileLock('/path/to/file.json', async () => {
|
||||||
|
* const data = await fs.readFile(filePath, 'utf8');
|
||||||
|
* const modified = JSON.parse(data);
|
||||||
|
* modified.key = 'new value';
|
||||||
|
* await fs.writeFile(filePath, JSON.stringify(modified, null, 2));
|
||||||
|
* return modified;
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export async function withFileLock(filePath, operation) {
|
||||||
|
const releaseLock = await acquireFileLock(filePath);
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} finally {
|
||||||
|
releaseLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件是否被锁定(有等待中的锁队列)
|
||||||
|
* @param {string} filePath - 文件路径
|
||||||
|
* @returns {boolean} 是否被锁定
|
||||||
|
*/
|
||||||
|
export function isFileLocked(filePath) {
|
||||||
|
const normalizedPath = path.resolve(filePath);
|
||||||
|
return fileLockQueues.has(normalizedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前被锁定的文件数量(用于调试)
|
||||||
|
* @returns {number} 被锁定的文件数量
|
||||||
|
*/
|
||||||
|
export function getLockedFileCount() {
|
||||||
|
return fileLockQueues.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
acquireFileLock,
|
||||||
|
withFileLock,
|
||||||
|
isFileLocked,
|
||||||
|
getLockedFileCount
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue