petro-kushchak / homebridge-homepod-radio

MIT License
39 stars 2 forks source link

Error while trying to stop file streaming #52

Open justjam2013 opened 1 month ago

justjam2013 commented 1 month ago

Installed version of homebridge-homepod-radio: v3.0.0 - latest

Seeing the following error in HomeBridge log:

[9/29/2024, 10:35:07 PM] [HomepodRadioPlatform] [File Play Button] Error while trying to stop: Error: Command failed: kill -9 322466 /bin/sh: 1: kill: No such process

This happens because when a file is played, playFile spawns a child process and sets a callback for when the child exits. When the child process terminates, it calls endStreaming, which calls killProcess with the PID of the child process which has already exited. Therefore kill -9 ${procId} will always fail to find a process with that PID.

/src/lib/airplayDevice.ts

  public async playFile(filePath: string, volume: number): Promise<boolean> {
      ...
      this.streaming = child.spawn(
          'python3',
          ...
      );

      ...

      this.streaming.on('exit', async (code, signal) => {
          this.logger.info(`[${this.streamerName}] streaming exit: code ${code} signal ${signal}`);
          await this.endStreaming();
      });

      ...
  }

  private async endStreaming(): Promise<boolean> {
      try {
          ...

          await this.killProcess(this.streaming.pid);
          this.streaming = null;
      } catch (err) {
          this.logger.error(`[${this.streamerName}] Error while trying to stop: ${err}`);
          this.streaming = null;
      }

      ...
  }

  private async killProcess(procId: number): Promise<void> {
      const cmd = `kill -9 ${procId}`;
      const result = await execAsync(cmd);
      this.debug(`[${this.streamerName}] Executing "${result}" result: ${JSON.stringify(result)}`);
  }

This could be fixed by passing a boolean flag to endStreaming() and only kill the subprocess if it's not a file stream:

  private async endStreaming(fileStream: boolean = false): Promise<boolean> {
      try {
          ...

          if (!fileStream) {
              await this.killProcess(this.streaming.pid);
          }
          this.streaming = null;
      } catch (err) {
          this.logger.error(`[${this.streamerName}] Error while trying to stop: ${err}`);
          this.streaming = null;
      }

      ...
  }

And calling

  public async playFile(filePath: string, volume: number): Promise<boolean> {
      ...

      this.streaming.on('exit', async (code, signal) => {
          this.logger.info(`[${this.streamerName}] streaming exit: code ${code} signal ${signal}`);
          await this.endStreaming(true);
      });

      ...
  }
justjam2013 commented 1 month ago

Noticed that startStreaming also sets an exit callback, so code update:

  private async endStreaming(streamExit: boolean = false): Promise<boolean> {
      try {
          ...

          if (!streamExit) {
              await this.killProcess(this.streaming.pid);
          }
          this.streaming = null;
      } catch (err) {
          this.logger.error(`[${this.streamerName}] Error while trying to stop: ${err}`);
          this.streaming = null;
      }
      return Promise.resolve(true);
  }

then:

  public async playFile(filePath: string, volume: number): Promise<boolean> {
      ...

      this.streaming.on('exit', async (code, signal) => {
          this.logger.info(`[${this.streamerName}] streaming exit: code ${code} signal ${signal}`);
          await this.endStreaming(true);
      });

      ...

      return true;
  }

and:

  private async startStreaming(
      streamUrl: string,
      streamName: string,
      volume: number,
      heartbeat: (source: string, heartbeatFailed: () => Promise<void>) => Promise<void>,
      heartbeatFailed: () => Promise<void>,
  ): Promise<boolean> {
      ...

      this.streaming.on('exit', async (code, signal) => {
          this.logger.info(`[${this.streamerName}] streaming exit: code ${code} signal ${signal}`);
          await this.endStreaming(true);
      });

      ...

      return true;
  }