Skip to content

fix: make session copy resilient by skipping locked files (EBUSY) #3546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

arturhc
Copy link

@arturhc arturhc commented May 7, 2025

Replaced the direct call to fs.copy() with a recursive, file-by-file resilient copy strategy.

This change prevents the session compression process from failing entirely when a single file is locked or in use (common with RemoteAuth files like .wwebjs_auth/RemoteAuth-artific/Default/Sessions/Session_13391086524750032).

  • Locked or in-use files (EBUSY) are now skipped with a warning.
  • All other session files are still copied correctly.
  • This improves compatibility on both Windows and Linux environments.
  • Could fix unexplained RemoteAuth issues that some users experienced when restoring sessions.

PR Details

Replaces the use of fs.copy() with a custom recursive copy function that skips over locked files (like .wwebjs_auth/RemoteAuth-artific/Default/Sessions/Session_13391086524750032 auth files) to prevent full session copy failure. This improves reliability when compressing sessions on systems where certain files may be held open.

Description

  • Introduced copyResilient(), a recursive method to copy directories file-by-file.
  • Wrapped each copy operation in a try/catch block.
  • When a file is locked or causes EBUSY, it is skipped and logged as a warning.
  • Replaced the single call to fs.copy() in compressSession() with this resilient approach.

Related Issue(s)

Possibly related to RemoteAuth session issues on Windows and Linux reported by various users.

Motivation and Context

Some users have reported intermittent failures when trying to backup or compress sessions, particularly when using RemoteAuth. This PR ensures that such failures no longer prevent the rest of the session from being saved.

How Has This Been Tested

  • Manually tested on Windows 11 with an active WhatsApp session.
  • Simulated file lock on .wwebjs_auth/RemoteAuth-artific/Default/Sessions/Session_13391086524750032 by keeping client open.
  • Verified that locked file is skipped, while remaining files are successfully copied and zipped.

Environment

  • Machine OS: Windows 11
  • Library Version: whatsapp-web.js@1.27.0
  • WhatsApp Web Version: 2.2407.3
  • Puppeteer Version: (default installed by whatsapp-web.js)
  • Browser Type and Version: (default bundled Chromium)
  • Node Version: 22.x

Types of changes

  • Dependency change
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My code follows the code style of this project.
  • I have updated the documentation accordingly (index.d.ts).
  • I have updated the usage example accordingly (example.js)

Replaced the direct call to fs.copy() with a recursive, file-by-file resilient copy strategy.

This change prevents the session compression process from failing entirely when a single file is locked or in use (common with RemoteAuth files like `.wwebjs_auth/RemoteAuth.plain`).

- Locked or in-use files (EBUSY) are now skipped with a warning.
- All other session files are still copied correctly.
- This improves compatibility on both Windows and Linux environments.
@arturhc
Copy link
Author

arturhc commented May 7, 2025

Hi @themazim @alechkos 👋

This is my first contribution to the project, so I really appreciate your patience if something isn’t 100% aligned with the PR conventions 🙏

I couldn’t find an open issue that directly addressed the error I was facing, but I wanted to share the context in case it helps others.

The issue was related to saving the RemoteAuth session, especially when files like .wwebjs_auth/RemoteAuth-artific/Default/Sessions/Session_13391086524750032 were locked — for example, while the client is still running. This would cause fs.copy() to fail with EBUSY, and result in an incomplete or totally broken session save.

This is the error:

Error: EBUSY: resource busy or locked, copyfile
'C:\Users\artur\Escritorio\wwjs-nest-test\.wwebjs_auth\RemoteAuth-artific\Default\Sessions\Session_13391086524750032'
->
'C:\Users\artur\Escritorio\wwjs-nest-test\.wwebjs_auth\wwebjs_temp_session_artific\Default\Sessions\Session_13391086524750032'
code:    'EBUSY'
errno:   -4082
syscall: 'copyfile'

image

To solve this, I implemented a resilient copy function that skips locked files and continues copying the rest. This dramatically improved session handling in my setup.

For context, here’s the custom RemoteAuth store I’m using. It fully complies with the Store interface from whatsapp-web.js and uses TypeORM to store session backups as zip buffers:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as fs from 'fs';
import { WhatsAppSession } from 'src/model/whatsapp-session.entity';

@Injectable()
export class TypeOrmStore {
  constructor(
    @InjectRepository(WhatsAppSession)
    private readonly repo: Repository<WhatsAppSession>,
  ) {}

  async getAllSessions(): Promise<{ sessionId: string }[]> {
    return this.repo.find({ select: ['sessionId'] });
  }

  async sessionExists({ session }: { session: string }): Promise<boolean> {
    return this.repo.exists({ where: { sessionId: this.getSessionId(session) } });
  }

  async save({ session }: { session: string }): Promise<void> {
    const zipPath = `${session}.zip`;
    const data = await fs.promises.readFile(zipPath);
    await this.repo.save({
      sessionId: this.getSessionId(session),
      data,
    });
  }

  async extract({ session, path }: { session: string; path: string }): Promise<void> {
    const row = await this.repo.findOneBy({
      sessionId: this.getSessionId(session),
    });
    if (!row) return;
    await fs.promises.writeFile(path, row.data);
  }

  async delete({ session }: { session: string }): Promise<void> {
    await this.repo.delete({
      sessionId: this.getSessionId(session),
    });
  }

  private getSessionId(session: string): string {
    return session.replaceAll('RemoteAuth-', '');
  }
}

@themazim
Copy link
Contributor

themazim commented May 7, 2025

Seems the lint gods are not yet happy, but we currently do not seem to log anything in the file. @alechkos will let you know how to handle this.

unfortunately I am not familiar with running this on windows, yet the solution seems to be a good addition as long as we do not skip necessary files. have you found any flaws when trying to restore a session? I would assume we may run into issues at some point for missing vital files.

@arturhc
Copy link
Author

arturhc commented May 7, 2025

@themazim .wwebjs_auth/RemoteAuth-artific/Default/Sessions/Session_13391086524750032
Aside from that, everything else is being copied correctly.
The purpose of resilientCopy is simply to ensure that everything that can be copied is copied.

Let me check the lint :)

@arturhc
Copy link
Author

arturhc commented May 9, 2025

@stevefold @themazim @jrocha @tegila who could follow up on this? :)

@shirser121
Copy link
Collaborator

When you not copy the locked files you make the session failed or not be up to date

try {
const stat = await fs.stat(srcPath);
if (stat.isDirectory()) {
await this.copyResilient(srcPath, destPath); // recursividad para subcarpetas
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No Spanish comments please

const srcPath = path.join(srcDir, item);
const destPath = path.join(destDir, item);

/*eslint no-empty: "error"*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the exception of eslint, let it give the error

@@ -145,7 +145,7 @@ class RemoteAuth extends BaseAuthStrategy {
const archive = archiver('zip');
const stream = fs.createWriteStream(`${this.sessionName}.zip`);

await fs.copy(this.userDataDir, this.tempDir).catch(() => {});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants