content: /*
   ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
   ~                                                                               ~
   ~ The MIT License (MIT)                                                         ~
   ~                                                                               ~
   ~ Copyright (c) 2015-2025 miaixz.org OSHI and other contributors.               ~
   ~                                                                               ~
   ~ Permission is hereby granted, free of charge, to any person obtaining a copy  ~
   ~ of this software and associated documentation files (the "Software"), to deal ~
   ~ in the Software without restriction, including without limitation the rights  ~
   ~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell     ~
   ~ copies of the Software, and to permit persons to whom the Software is         ~
   ~ furnished to do so, subject to the following conditions:                      ~
   ~                                                                               ~
   ~ The above copyright notice and this permission notice shall be included in    ~
   ~ all copies or substantial portions of the Software.                           ~
   ~                                                                               ~
   ~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR    ~
   ~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,      ~
   ~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE   ~
   ~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER        ~
   ~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ~
   ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN     ~
   ~ THE SOFTWARE.                                                                 ~
   ~                                                                               ~
   ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
  */
  package org.miaixz.bus.health.mac.software;
  
  import java.io.File;
  import java.util.*;
  import java.util.function.Supplier;
  import java.util.stream.Collectors;
  
  import org.miaixz.bus.core.lang.Normal;
  import org.miaixz.bus.core.lang.Symbol;
  import org.miaixz.bus.core.lang.annotation.ThreadSafe;
  import org.miaixz.bus.core.lang.tuple.Pair;
  import org.miaixz.bus.core.xyz.StringKit;
  import org.miaixz.bus.health.Config;
  import org.miaixz.bus.health.Executor;
  import org.miaixz.bus.health.Memoizer;
  import org.miaixz.bus.health.Parsing;
  import org.miaixz.bus.health.builtin.jna.Struct;
  import org.miaixz.bus.health.builtin.software.*;
  import org.miaixz.bus.health.builtin.software.common.AbstractOperatingSystem;
  import org.miaixz.bus.health.mac.SysctlKit;
  import org.miaixz.bus.health.mac.driver.Who;
  import org.miaixz.bus.health.mac.driver.WindowInfo;
  import org.miaixz.bus.logger.Logger;
  
  import com.sun.jna.platform.mac.SystemB;
  
  /**
   * macOS, previously Mac OS X and later OS X) is a series of proprietary graphical operating systems developed and
   * marketed by Apple Inc. since 2001. It is the primary operating system for Apple's Mac computers.
   *
   * @author Kimi Liu
   * @since Java 17+
   */
  @ThreadSafe
  public class MacOperatingSystem extends AbstractOperatingSystem {
  
      private static final String SYSTEM_LIBRARY_LAUNCH_AGENTS = "/System/Library/LaunchAgents";
      private static final String SYSTEM_LIBRARY_LAUNCH_DAEMONS = "/System/Library/LaunchDaemons";
      private static final long BOOTTIME;
      private final Supplier<List<ApplicationInfo>> installedAppsSupplier = Memoizer
              .memoize(MacInstalledApps::queryInstalledApps, Memoizer.installedAppsExpiration());
  
      static {
          try (Struct.CloseableTimeval tv = new Struct.CloseableTimeval()) {
              if (!SysctlKit.sysctl("kern.boottime", tv) || tv.tv_sec.longValue() == 0L) {
                  // Usually this works. If it doesn't, fall back to text parsing.
                  // Boot time will be the first consecutive string of digits.
                  BOOTTIME = Parsing.parseLongOrDefault(
                          Executor.getFirstAnswer("sysctl -n kern.boottime").split(Symbol.COMMA)[0]
                                  .replaceAll("\\D", Normal.EMPTY),
                          System.currentTimeMillis() / 1000);
              } else {
                  // tv now points to a 64-bit timeval structure for boot time.
                  // First 4 bytes are seconds, second 4 bytes are microseconds
                  // (we ignore)
                  BOOTTIME = tv.tv_sec.longValue();
              }
          }
      }
  
      protected final String osXVersion;
      protected final int major;
      protected final int minor;
      protected int maxProc;
  
      public MacOperatingSystem() {
          this(SysctlKit.sysctl("kern.maxproc", 0x1000));
      }
  
      protected MacOperatingSystem(int maxproc) {
          String version = System.getProperty("os.version");
          int verMajor = Parsing.getFirstIntValue(version);
          int verMinor = Parsing.getNthIntValue(version, 2);
          // Big Sur (11.x) may return 10.16
          if (verMajor == 10 && verMinor > 15) {
              String swVers = Executor.getFirstAnswer("sw_vers -productVersion");
              if (!swVers.isEmpty()) {
                  version = swVers;
              }
              verMajor = Parsing.getFirstIntValue(version);
              verMinor = Parsing.getNthIntValue(version, 2);
          }
          this.osXVersion = version;
          this.major = verMajor;
          this.minor = verMinor;
          // Set max processes
          this.maxProc = maxproc;
      }
  
      @Override
      public String queryManufacturer() {
          return "Apple";
      }
  
      @Override
      public Pair<String, OperatingSystem.OSVersionInfo> queryFamilyVersionInfo() {
          String family = this.major > 10 || (this.major == 10 && this.minor >= 12) ? "macOS"
                  : System.getProperty("os.name");
          String codeName = parseCodeName();
          String buildNumber = SysctlKit.sysctl("kern.osversion", Normal.EMPTY);
          return Pair.of(family, new OperatingSystem.OSVersionInfo(this.osXVersion, codeName, buildNumber));
      }
  
      private String parseCodeName() {
          Properties verProps = Config.readProperties(Config._MACOS_VERSIONS_PROPERTIES);
          String codeName = null;
          if (this.major > 10) {
              codeName = verProps.getProperty(Integer.toString(this.major));
          } else if (this.major == 10) {
              codeName = verProps.getProperty(this.major + "." + this.minor);
          }
          if (StringKit.isBlank(codeName)) {
              Logger.warn("Unable to parse version {}.{} to a codename.", this.major, this.minor);
          }
          return codeName;
      }
  
      @Override
      protected int queryBitness(int jvmBitness) {
          if (jvmBitness == 64 || (this.major == 10 && this.minor > 6)) {
              return 64;
          }
          return Parsing.parseIntOrDefault(Executor.getFirstAnswer("getconf LONG_BIT"), 32);
      }
  
      @Override
      public FileSystem getFileSystem() {
          return new MacFileSystem();
      }
  
      @Override
      public InternetProtocolStats getInternetProtocolStats() {
          return new MacInternetProtocolStats(isElevated());
      }
  
      @Override
      public List<OSSession> getSessions() {
          return USE_WHO_COMMAND ? super.getSessions() : Who.queryUtxent();
      }
  
      @Override
      public List<OSProcess> queryAllProcesses() {
          List<OSProcess> procs = new ArrayList<>();
          int[] pids = new int[this.maxProc];
          Arrays.fill(pids, -1);
          int numberOfProcesses = SystemB.INSTANCE
                  .proc_listpids(SystemB.PROC_ALL_PIDS, 0, pids, pids.length * SystemB.INT_SIZE) / SystemB.INT_SIZE;
          for (int i = 0; i < numberOfProcesses; i++) {
              if (pids[i] >= 0) {
                  OSProcess proc = getProcess(pids[i]);
                  if (proc != null) {
                      procs.add(proc);
                  }
              }
          }
          return procs;
      }
  
      @Override
      public OSProcess getProcess(int pid) {
          OSProcess proc = new MacOSProcess(pid, this.major, this.minor, this);
          return proc.getState().equals(OSProcess.State.INVALID) ? null : proc;
      }
  
      @Override
      public List<OSProcess> queryChildProcesses(int parentPid) {
          List<OSProcess> allProcs = queryAllProcesses();
          Set<Integer> descendantPids = getChildrenOrDescendants(allProcs, parentPid, false);
          return allProcs.stream().filter(p -> descendantPids.contains(p.getProcessID())).collect(Collectors.toList());
      }
  
      @Override
      public List<OSProcess> queryDescendantProcesses(int parentPid) {
          List<OSProcess> allProcs = queryAllProcesses();
          Set<Integer> descendantPids = getChildrenOrDescendants(allProcs, parentPid, true);
          return allProcs.stream().filter(p -> descendantPids.contains(p.getProcessID())).collect(Collectors.toList());
      }
  
      @Override
      public int getProcessId() {
          return SystemB.INSTANCE.getpid();
      }
  
      @Override
      public int getProcessCount() {
          return SystemB.INSTANCE.proc_listpids(SystemB.PROC_ALL_PIDS, 0, null, 0) / SystemB.INT_SIZE;
      }
  
      @Override
      public int getThreadId() {
          OSThread thread = getCurrentThread();
          if (thread == null) {
              return 0;
          }
          return thread.getThreadId();
      }
  
      @Override
      public OSThread getCurrentThread() {
          // Get oldest thread
          return getCurrentProcess().getThreadDetails().stream().sorted(Comparator.comparingLong(OSThread::getStartTime))
                  .findFirst().orElse(new MacOSThread(getProcessId()));
      }
  
      @Override
      public int getThreadCount() {
          // Get current pids, then slightly pad in case new process starts while
          // allocating array space
          int[] pids = new int[getProcessCount() + 10];
          int numberOfProcesses = SystemB.INSTANCE.proc_listpids(SystemB.PROC_ALL_PIDS, 0, pids, pids.length)
                  / SystemB.INT_SIZE;
          int numberOfThreads = 0;
          try (Struct.CloseableProcTaskInfo taskInfo = new Struct.CloseableProcTaskInfo()) {
              for (int i = 0; i < numberOfProcesses; i++) {
                  int exit = SystemB.INSTANCE
                          .proc_pidinfo(pids[i], SystemB.PROC_PIDTASKINFO, 0, taskInfo, taskInfo.size());
                  if (exit != -1) {
                      numberOfThreads += taskInfo.pti_threadnum;
                  }
              }
          }
          return numberOfThreads;
      }
  
      @Override
      public long getSystemUptime() {
          return System.currentTimeMillis() / 1000 - getSystemBootTime();
      }
  
      @Override
      public long getSystemBootTime() {
          return BOOTTIME;
      }
  
      @Override
      public NetworkParams getNetworkParams() {
          return new MacNetworkParams();
      }
  
      @Override
      public List<OSService> getServices() {
          // Get running services
          List<OSService> services = new ArrayList<>();
          Set<String> running = new HashSet<>();
          for (OSProcess p : getChildProcesses(
                  1,
                  OperatingSystem.ProcessFiltering.ALL_PROCESSES,
                  OperatingSystem.ProcessSorting.PID_ASC,
                  0)) {
              OSService s = new OSService(p.getName(), p.getProcessID(), OSService.State.RUNNING);
              services.add(s);
              running.add(p.getName());
          }
          // Get Directories for stopped services
          ArrayList<File> files = new ArrayList<>();
          File dir = new File(SYSTEM_LIBRARY_LAUNCH_AGENTS);
          if (dir.exists() && dir.isDirectory()) {
              files.addAll(Arrays.asList(dir.listFiles((f, name) -> name.toLowerCase(Locale.ROOT).endsWith(".plist"))));
          } else {
              Logger.error("Directory: /System/Library/LaunchAgents does not exist");
          }
          dir = new File(SYSTEM_LIBRARY_LAUNCH_DAEMONS);
          if (dir.exists() && dir.isDirectory()) {
              files.addAll(Arrays.asList(dir.listFiles((f, name) -> name.toLowerCase(Locale.ROOT).endsWith(".plist"))));
          } else {
              Logger.error("Directory: /System/Library/LaunchDaemons does not exist");
          }
          for (File f : files) {
              // remove .plist extension
              String name = f.getName().substring(0, f.getName().length() - 6);
              int index = name.lastIndexOf('.');
              String shortName = (index < 0 || index > name.length() - 2) ? name : name.substring(index + 1);
              if (!running.contains(name) && !running.contains(shortName)) {
                  OSService s = new OSService(name, 0, OSService.State.STOPPED);
                  services.add(s);
              }
          }
          return services;
      }
  
      @Override
      public List<OSDesktopWindow> getDesktopWindows(boolean visibleOnly) {
          return WindowInfo.queryDesktopWindows(visibleOnly);
      }
  
      @Override
      public List<ApplicationInfo> getInstalledApplications() {
          return installedAppsSupplier.get();
      }
  
  }
  