export function devicePlugin()

in desktop/plugins/public/cpu/index.tsx [73:375]


export function devicePlugin(client: PluginClient<{}, {}>) {
  const device = client.device;

  const executeShell = async (command: string) => device.executeShell(command);

  let intervalID: NodeJS.Timer | null = null;
  const cpuState = createState<CPUState>({
    cpuCount: 0,
    cpuFreq: [],
    monitoring: false,
    hardwareInfo: '',
    temperatureMap: {},
    thermalAccessible: true,
    displayThermalInfo: false,
    displayCPUDetail: true,
  });

  const updateCoreFrequency: (
    core: number,
    type: string,
  ) => Promise<void> = async (core: number, type: string) => {
    const output = await executeShell(
      'cat /sys/devices/system/cpu/cpu' + core + '/cpufreq/' + type,
    );
    cpuState.update((draft) => {
      const newFreq = isNormalInteger(output) ? parseInt(output, 10) : -1;
      // update table only if frequency changed
      if (draft.cpuFreq[core][type] != newFreq) {
        draft.cpuFreq[core][type] = newFreq;
        if (type == 'scaling_cur_freq' && draft.cpuFreq[core][type] < 0) {
          // cannot find current freq means offline
          draft.cpuFreq[core][type] = -2;
        }
      }
    });
  };

  const updateAvailableFrequencies: (core: number) => Promise<void> = async (
    core: number,
  ) => {
    const output = await executeShell(
      'cat /sys/devices/system/cpu/cpu' +
        core +
        '/cpufreq/scaling_available_frequencies',
    );
    cpuState.update((draft) => {
      const freqs = output.split(' ').map((num: string) => {
        return parseInt(num, 10);
      });
      draft.cpuFreq[core].scaling_available_freqs = freqs;
      const maxFreq = draft.cpuFreq[core].scaling_max_freq;
      if (maxFreq > 0 && freqs.indexOf(maxFreq) == -1) {
        freqs.push(maxFreq); // always add scaling max to available frequencies
      }
    });
  };

  const updateCoreGovernor: (core: number) => Promise<void> = async (
    core: number,
  ) => {
    const output = await executeShell(
      'cat /sys/devices/system/cpu/cpu' + core + '/cpufreq/scaling_governor',
    );
    cpuState.update((draft) => {
      if (output.toLowerCase().includes('no such file')) {
        draft.cpuFreq[core].scaling_governor = 'N/A';
      } else {
        draft.cpuFreq[core].scaling_governor = output;
      }
    });
  };

  const readAvailableGovernors: (core: number) => Promise<string[]> = async (
    core: number,
  ) => {
    const output = await executeShell(
      'cat /sys/devices/system/cpu/cpu' +
        core +
        '/cpufreq/scaling_available_governors',
    );
    return output.split(' ');
  };

  const readCoreFrequency = async (core: number) => {
    const freq = cpuState.get().cpuFreq[core];
    const promises = [];
    if (freq.cpuinfo_max_freq < 0) {
      promises.push(updateCoreFrequency(core, 'cpuinfo_max_freq'));
    }
    if (freq.cpuinfo_min_freq < 0) {
      promises.push(updateCoreFrequency(core, 'cpuinfo_min_freq'));
    }
    promises.push(updateCoreFrequency(core, 'scaling_cur_freq'));
    promises.push(updateCoreFrequency(core, 'scaling_min_freq'));
    promises.push(updateCoreFrequency(core, 'scaling_max_freq'));
    return Promise.all(promises).then(() => {});
  };

  const updateHardwareInfo = async () => {
    const output = await executeShell('getprop ro.board.platform');
    let hwInfo = '';
    if (
      output.startsWith('msm') ||
      output.startsWith('apq') ||
      output.startsWith('sdm')
    ) {
      hwInfo = 'QUALCOMM ' + output.toUpperCase();
    } else if (output.startsWith('exynos')) {
      const chipname = await executeShell('getprop ro.chipname');
      if (chipname != null) {
        cpuState.update((draft) => {
          draft.hardwareInfo = 'SAMSUMG ' + chipname.toUpperCase();
        });
      }
      return;
    } else if (output.startsWith('mt')) {
      hwInfo = 'MEDIATEK ' + output.toUpperCase();
    } else if (output.startsWith('sc')) {
      hwInfo = 'SPREADTRUM ' + output.toUpperCase();
    } else if (output.startsWith('hi') || output.startsWith('kirin')) {
      hwInfo = 'HISILICON ' + output.toUpperCase();
    } else if (output.startsWith('rk')) {
      hwInfo = 'ROCKCHIP ' + output.toUpperCase();
    } else if (output.startsWith('bcm')) {
      hwInfo = 'BROADCOM ' + output.toUpperCase();
    }
    cpuState.update((draft) => {
      draft.hardwareInfo = hwInfo;
    });
  };

  const readThermalZones = async () => {
    const thermal_dir = '/sys/class/thermal/';
    const map = {};
    const output = await executeShell('ls ' + thermal_dir);
    if (output.toLowerCase().includes('permission denied')) {
      cpuState.update((draft) => {
        draft.thermalAccessible = false;
      });
      return;
    }
    const dirs = output.split(/\s/);
    const promises = [];
    for (let d of dirs) {
      d = d.trim();
      if (d.length == 0) {
        continue;
      }
      const path = thermal_dir + d;
      promises.push(readThermalZone(path, d, map));
    }
    await Promise.all(promises);
    cpuState.update((draft) => {
      draft.temperatureMap = map;
      draft.thermalAccessible = true;
    });
    if (cpuState.get().displayThermalInfo) {
      setTimeout(readThermalZones, 1000);
    }
  };

  const readThermalZone = async (path: string, dir: string, map: any) => {
    const type = await executeShell('cat ' + path + '/type');
    if (type.length == 0) {
      return;
    }
    const temp = await executeShell('cat ' + path + '/temp');
    if (Number.isNaN(Number(temp))) {
      return;
    }
    map[type] = {
      path: dir,
      temp: parseInt(temp, 10),
    };
  };

  const onStartMonitor = () => {
    if (cpuState.get().monitoring) {
      return;
    }

    cpuState.update((draft) => {
      draft.monitoring = true;
    });

    for (let i = 0; i < cpuState.get().cpuCount; ++i) {
      readAvailableGovernors(i)
        .then((output) => {
          cpuState.update((draft) => {
            draft.cpuFreq[i].scaling_available_governors = output;
          });
        })
        .catch((e) => {
          console.error('Failed to read CPU governors:', e);
        });
    }

    const update = async () => {
      if (!cpuState.get().monitoring) {
        return;
      }
      const promises = [];
      for (let i = 0; i < cpuState.get().cpuCount; ++i) {
        promises.push(readCoreFrequency(i));
        promises.push(updateCoreGovernor(i));
        promises.push(updateAvailableFrequencies(i)); // scaling max might change, so we also update this
      }
      await Promise.all(promises);
      intervalID = setTimeout(update, 500);
    };

    intervalID = setTimeout(update, 500);
  };

  const onStopMonitor = () => {
    intervalID && clearInterval(intervalID);
    intervalID = null;
    cpuState.update((draft) => {
      draft.monitoring = false;
    });
  };

  const cleanup = () => {
    onStopMonitor();
    cpuState.update((draft) => {
      for (let i = 0; i < draft.cpuCount; ++i) {
        draft.cpuFreq[i].scaling_cur_freq = -1;
        draft.cpuFreq[i].scaling_min_freq = -1;
        draft.cpuFreq[i].scaling_max_freq = -1;
        draft.cpuFreq[i].scaling_available_freqs = [];
        draft.cpuFreq[i].scaling_governor = 'N/A';
        // we don't cleanup cpuinfo_min_freq, cpuinfo_max_freq
        // because usually they are fixed (hardware)
      }
    });
  };

  const toggleThermalSidebar = () => {
    if (!cpuState.get().displayThermalInfo) {
      readThermalZones();
    }
    cpuState.update((draft) => {
      draft.displayThermalInfo = !draft.displayThermalInfo;
      draft.displayCPUDetail = false;
    });
  };

  const toggleCPUSidebar = () => {
    cpuState.update((draft) => {
      draft.displayCPUDetail = !draft.displayCPUDetail;
      draft.displayThermalInfo = false;
    });
  };

  // check how many cores we have on this device
  executeShell('cat /sys/devices/system/cpu/possible')
    .then((output) => {
      const idx = output.indexOf('-');
      const cpuFreq = [];
      const count = parseInt(output.substring(idx + 1), 10) + 1;
      for (let i = 0; i < count; ++i) {
        cpuFreq[i] = {
          cpu_id: i,
          scaling_cur_freq: -1,
          scaling_min_freq: -1,
          scaling_max_freq: -1,
          cpuinfo_min_freq: -1,
          cpuinfo_max_freq: -1,
          scaling_available_freqs: [],
          scaling_governor: 'N/A',
          scaling_available_governors: [],
        };
      }
      cpuState.set({
        cpuCount: count,
        cpuFreq: cpuFreq,
        monitoring: false,
        hardwareInfo: '',
        temperatureMap: {},
        thermalAccessible: true,
        displayThermalInfo: false,
        displayCPUDetail: true,
      });
    })
    .catch((e) => {
      console.error('Failed to read CPU cores:', e);
    });

  client.onDeactivate(() => cleanup());
  client.onActivate(() => {
    updateHardwareInfo();
    readThermalZones();
  });

  return {
    executeShell,
    cpuState,
    onStartMonitor,
    onStopMonitor,
    toggleCPUSidebar,
    toggleThermalSidebar,
  };
}