Skip to content
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

Wait till process stopped #23

Open
nick4fake opened this issue Jan 7, 2023 · 0 comments
Open

Wait till process stopped #23

nick4fake opened this issue Jan 7, 2023 · 0 comments

Comments

@nick4fake
Copy link

These features would be really nice to have:

  • Wait till process stopped during restart
  • Kill process with SIGKILL if it didn't stop during timeout

Example:

import {ChildProcess, fork} from 'child_process';
import {Compilation, Compiler, WebpackPluginInstance} from 'webpack';

const killProcess = async ({pid, signal = 'SIGTERM', timeout}) => {
  process.kill(pid, signal);
  let count = 0;
  do {
    try {
      process.kill(pid, 0);
    } catch (e) {
      return;
    }
    if ((count += 100) > timeout) {
      break;
    }

    await new Promise(cb => setTimeout(cb, 100));
  } while (true);

  try {
    process.kill(pid, 'SIGKILL');
  } catch (e) {
    return;
  }

  count = 0;
  do {
    try {
      process.kill(pid, 0);
    } catch (e) {
      return;
    }
    if ((count += 100) > timeout) {
      throw new Error('Timeout process kill');
    }

    await new Promise(cb => setTimeout(cb, 100));
  } while (true);

};

export type RunScriptWebpackPluginOptions = {
  autoRestart?: boolean;
  args: string[];
  cwd?: string;
  keyboard: boolean;
  name?: string;
  nodeArgs: string[];
  restartable?: boolean;
  signal: boolean | string;
  killTimeoutMs?: number;
};

function getSignal(signal: string | boolean) {
  // allow users to disable sending a signal by setting to `false`...
  if (signal === false) return;
  if (signal === true) return 'SIGUSR2';
  return signal;
}

export class RunScriptWebpackPlugin implements WebpackPluginInstance {
  private readonly options: RunScriptWebpackPluginOptions;

  private worker?: ChildProcess;

  private _entrypoint?: string;

  constructor(options: Partial<RunScriptWebpackPluginOptions> = {}) {
    this.options = {
      autoRestart: true,
      signal: false,
      killTimeoutMs: 5000,
      // Only listen on keyboard in development, so the server doesn't hang forever
      keyboard: process.env.NODE_ENV === 'development',
      ...options,
      args: [...(options.args || [])],
      nodeArgs: options.nodeArgs || process.execArgv,
    };

    if (this.options.restartable) {
      this._enableRestarting();
    }
  }

  private _enableRestarting(): void {
    if (this.options.keyboard) {
      process.stdin.setEncoding('utf8');
      process.stdin.on('data', (data: string) => {
        if (data.trim() === 'rs') {
          this._restartServer();
        }
      });
    }
  }

  private async _restartServer(): Promise<void> {
    console.log('Restarting app...');
    if (this.worker?.pid) {
      const signal = getSignal(this.options.signal);
      await killProcess({
        pid: this.worker.pid,
        signal,
        timeout: this.options.killTimeoutMs,
      });
    }
    this._startServer((worker) => {
      this.worker = worker;
    });
  }

  private afterEmit = (compilation: Compilation, cb: (err?: any) => void): void => {
    if (this.worker && this.worker.connected && this.worker?.pid) {
      if (this.options.autoRestart) {
        this._restartServer().then(() => cb()).catch(err => cb(err));
        return;
      }
      const signal = getSignal(this.options.signal);
      if (signal) {
        killProcess({
          pid: this.worker.pid,
          signal,
          timeout: this.options.killTimeoutMs,
        }).then(() => cb()).catch(err => cb(err));
      }
      cb();
      return;
    }

    this.startServer(compilation, cb);
  };

  apply = (compiler: Compiler): void => {
    compiler.hooks.afterEmit.tapAsync(
      {name: 'RunScriptPlugin'},
      this.afterEmit,
    );
  };

  private startServer = (compilation: Compilation, cb: () => void): void => {
    const {assets, compiler} = compilation;
    const {options} = this;
    let name;
    const names = Object.keys(assets);
    if (options.name) {
      name = options.name;
      if (!assets[name]) {
        console.error(
          `Entry ${name} not found. Try one of: ${names.join(' ')}`,
        );
      }
    } else {
      name = names[0];
      if (names.length > 1) {
        console.log(
          `More than one entry built, selected ${name}. All names: ${names.join(
            ' ',
          )}`,
        );
      }
    }
    if (!compiler.options.output || !compiler.options.output.path) {
      throw new Error('output.path should be defined in webpack config!');
    }

    this._entrypoint = `${compiler.options.output.path}/${name}`;
    this._startServer((worker) => {
      this.worker = worker;
      cb();
    });
  };

  private _startServer(cb: (arg0: ChildProcess) => void): void {
    const {args, nodeArgs, cwd} = this.options;
    if (!this._entrypoint) throw new Error('run-script-webpack-plugin requires an entrypoint.');

    const child = fork(this._entrypoint, args, {
      execArgv: nodeArgs,
      stdio: 'inherit',
      cwd,
    });
    setTimeout(() => cb(child), 0);
  }
}
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

No branches or pull requests

1 participant