backend/analyzer/installedIDEs/InstallationsFinder.go (366 lines of code) (raw):

package installedIDEs import ( "bufio" "encoding/json" "errors" "fmt" "io/ioutil" "log" "net/http" "os" "path/filepath" "runtime" "sort" "strings" "sync" "time" ) type ideInfoFromDebugger struct { Name string `json:"name"` ProductName string `json:"productName"` BuildNumber string `json:"buildNumber"` } type IdeInfo struct { Name string Version string BuildNumber string ProductCode string DataDirectoryName string LogsDirectory string IsRepairBundled bool Launch []struct { Os string `json:"os"` LauncherPath string `json:"launcherPath"` JavaExecutablePath string `json:"javaExecutablePath"` VmOptionsFilePath string `json:"vmOptionsFilePath"` } `json:"launch"` } type IDE struct { Binary string Package string Running bool Info IdeInfo } var ( IdePropertiesMap = map[string]string{} IdeProductInfoRelatedToInstallationPath = map[string]string{ "darwin": "/Contents/Resources/product-info.json", "linux": "/product-info.json", "windows": "/product-info.json", } possibleBaseFileNames = []string{"appcode", "clion", "datagrip", "dataspell", "goland", "idea", "phpstorm", "pycharm", "rubymine", "webstorm", "rider", "Draft"} IdeBinaryRelatedToInstallationPath = map[string]string{ "darwin": "/Contents/MacOS/{possibleBaseFileName}", "linux": "/bin/{possibleBaseFileName}.sh", "windows": "/bin/{possibleBaseFileName}64.exe", } possibleBinariesPaths = map[string][]string{ "darwin": {"/Applications/*.app/Contents/MacOS/{possibleBaseFileName}", "$HOME/Library/Application Support/JetBrains/Toolbox/apps/*/ch-*/*/*.app/Contents/MacOS/{possibleBaseFileName}"}, "linux": {"$HOME/.local/share/JetBrains/Toolbox/apps/*/ch-*/*/bin/{possibleBaseFileName}.sh"}, "windows": {os.Getenv("HOMEDRIVE") + "/Program Files/JetBrains/*" + IdeBinaryRelatedToInstallationPath["windows"], os.Getenv("LOCALAPPDATA") + "/JetBrains/Toolbox/apps/*/ch-*/*" + IdeBinaryRelatedToInstallationPath["windows"]}, } defaultLogsDirLocation = map[string]string{ "darwin": UserHomeDir() + "/Library/Logs/JetBrains/{dataDirectoryName}/", "linux": UserHomeDir() + "/.cache/JetBrains/{dataDirectoryName}/log/", "windows": os.Getenv("LOCALAPPDATA") + "/JetBrains/{dataDirectoryName}/log/", } possibleIdeaPropertiesFileLocations = map[string][]string{ "darwin": {"${IDE_BasefileName}_PROPERTIES", UserHomeDir() + "/Library/Application Support/JetBrains/{dataDirectoryName}/idea.properties", UserHomeDir() + "/idea.properties", "{ideaPackage}/Contents/bin/idea.properties"}, "linux": {"${IDE_BasefileName}_PROPERTIES", UserHomeDir() + "/.config/JetBrains/{dataDirectoryName}/idea.properties", UserHomeDir() + "/idea.properties", "{ideaPackage}/bin/idea.properties"}, "windows": {"${IDE_BasefileName}_PROPERTIES", defaultSystemDirLocation[runtime.GOOS] + "/idea.properties", UserHomeDir() + "/idea.properties", "{ideaPackage}/bin/idea.properties"}, } defaultSystemDirLocation = map[string]string{ "darwin": "${HOME}/Library/Caches/JetBrains/{dataDirectoryName}/", "linux": "${HOME}/.cache/JetBrains/{dataDirectoryName}/", "windows": os.Getenv("LOCALAPPDATA") + "/JetBrains/{dataDirectoryName}/", } ) func GetIdeInstallations() (ides []IDE) { runningIDEs := getRunningIdes() log.Printf("Scanning system for IDE installations") var installedIdes []string installedIdes, _ = findInstalledIdePackages() for _, idePackage := range installedIdes { info, _ := getIdeInfoByPackage(idePackage) binary, _ := getIdeBinaryByPackage(idePackage) info.LogsDirectory = getIdeLogsDir(binary) isRunning := checkIfInstallationRunning(runningIDEs, info) if info.LogsDirectory != "" { ides = append(ides, IDE{ Binary: binary, Package: idePackage, Running: isRunning, Info: info, }) //log.Printf("[runnning: %v] [%v] %v %v (%v-%v) - %v \n", isRunning, i, info.Name, info.Version, info.ProductCode, info.BuildNumber, beautifyPackageName(idePackage)) } } //sort them by running state. Running ones first sort.Slice(ides, func(i int, j int) bool { return ides[i].Running }) return ides } func checkIfInstallationRunning(runningIDEs []ideInfoFromDebugger, info IdeInfo) bool { for _, e := range runningIDEs { if e.BuildNumber == info.BuildNumber && strings.Contains(info.Name, e.ProductName) { return true } } return false } func getRunningIdes() (ides []ideInfoFromDebugger) { var wg sync.WaitGroup for i := 63342; i < 63392; i++ { wg.Add(1) go func(i int) { url := fmt.Sprintf("http://localhost:%d/api/about", i) if ideInfo, err := getIdeInfoFromPort(url); err == nil { ides = append(ides, ideInfo) } defer wg.Done() }(i) } wg.Wait() return ides } func getIdeInfoFromPort(url string) (ideInfoFromDebugger, error) { client := http.Client{ Timeout: time.Second, } res, err := client.Get(url) if err != nil { //log.Printf("Error getting HTTP response. err: %s", err.Error()) return ideInfoFromDebugger{}, err } var ideInstance ideInfoFromDebugger content, _ := ioutil.ReadAll(res.Body) _ = res.Body.Close() ideInstance = parseRunningIdeInfo(content) log.Printf("Found running IDE %v at url %v", ideInstance, url) return ideInstance, err } func parseRunningIdeInfo(body []byte) ideInfoFromDebugger { ideInfo := ideInfoFromDebugger{} jsonErr := json.Unmarshal(body, &ideInfo) if jsonErr != nil { log.Printf("Could not unmarshall JSON, %s", jsonErr.Error()) } return ideInfo } func findInstalledIdePackages() (installedIdes []string, err error) { for _, path := range getOsDependentDir(possibleBinariesPaths) { var foundInstallations []string foundInstallations, err = findIdeInstallationsByMask(path) installedIdes = append(installedIdes, foundInstallations...) } return installedIdes, err } func getOsDependentDir(fromVariable map[string][]string) []string { if len(fromVariable[runtime.GOOS]) > 0 { return fromVariable[runtime.GOOS] } log.Printf("This OS is not yet supported") return nil } func findIdeInstallationsByMask(path string) (foundIdePackages []string, err error) { for _, possibleBaseFileName := range possibleBaseFileNames { currentPath := strings.Replace(path, "{possibleBaseFileName}", possibleBaseFileName, -1) matches, _ := filepath.Glob(os.ExpandEnv(currentPath)) for _, match := range matches { match = getIdeIdePackageByBinary(match) foundIdePackages = append(foundIdePackages, match) } } return foundIdePackages, err } func getIdeIdePackageByBinary(ideaBinary string) (ideaPackage string) { if ideaPackageToWorkWith, err := detectInstallationByInnerPath(ideaBinary, false); err == nil { return ideaPackageToWorkWith } else { log.Printf("Could not get detect ide installation path by binary %s", ideaBinary) return "" } } //If any part of providedPath is IDE installation path, detectInstallationByInnerPath returns path or binary (based on returnBinary flag) func detectInstallationByInnerPath(providedPath string, returnBinary bool) (ideaBinary string, err error) { providedPath = filepath.Clean(providedPath) providedDeep := strings.Count(providedPath, string(os.PathSeparator)) basePath := providedPath for i := 1; i < providedDeep; i++ { if ideaBinary, err := getIdeBinaryByPackage(basePath); err == nil { if returnBinary { return ideaBinary, nil } else { return basePath, nil } } basePath = filepath.Dir(basePath) } return "", errors.New("Could not detect IDE by \"" + providedPath + "\" path") } //getIdeBinaryByPackage return the location of idea(idea.exe) executable inside the IDE installation folder. //if ideaPackage == /Users/konstantin.annikov/Downloads/IntelliJ IDEA.app //then idaBinary == /Users/konstantin.annikov/Downloads/IntelliJ IDEA.app/Contents/MacOS/idea func getIdeBinaryByPackage(ideaPackage string) (ideaBinary string, err error) { for _, possibleBaseFileName := range possibleBaseFileNames { for operatingSystem, path := range IdeBinaryRelatedToInstallationPath { currentBinaryToCheck := strings.Replace(path, "{possibleBaseFileName}", possibleBaseFileName, -1) ideaBinary = ideaPackage + currentBinaryToCheck if FileExists(ideaBinary) { if operatingSystem != runtime.GOOS { log.Printf("Provided path is for %s, but repair utility is running at %s ", operatingSystem, runtime.GOOS) } return filepath.Clean(ideaBinary), nil } } } //log.Printf(("Could not detect IDE binary in " + ideaPackage)) return "", errors.New("Could not detect IDE binary") } func getIdeInfoByPackage(ideaPackage string) (parameterValue IdeInfo, err error) { var a IdeInfo var fileContent []byte fileContent, err = ioutil.ReadFile(ideaPackage + IdeProductInfoRelatedToInstallationPath[runtime.GOOS]) if err != nil { for currentOs, path := range IdeProductInfoRelatedToInstallationPath { if content, er := ioutil.ReadFile(ideaPackage + path); er == nil { fileContent = content log.Printf("Could not find product-info.json for %s, but found it for %s ", runtime.GOOS, currentOs) } } } err = json.Unmarshal(fileContent, &a) return a, err } func getIdeLogsDir(ideaBinary string) (logsDir string) { if value := GetIdePropertyByName("idea.log.path", ideaBinary); len(value) != 0 { if FileExists(value) { return value } else { log.Printf("'idea.log.path' property is defined, but directory \"%s\" does not exist", value) } } installationInfo, err := getIdeInfoByBinary(ideaBinary) if err != nil { log.Printf("getIdeInfoByBinary failed. ideaBinary: %s, Error: %s", ideaBinary, err) } logsDir = strings.Replace(defaultLogsDirLocation[runtime.GOOS], "{dataDirectoryName}", installationInfo.DataDirectoryName, -1) logsDir = os.ExpandEnv(logsDir) if FileExists(logsDir) { return logsDir } else { log.Printf("Could not detect logs directory location for %s. Maybe it has never run?", ideaBinary) return "" } } func GetIdePropertyByName(name string, ideaBinary string) (value string) { if len(IdePropertiesMap) == 0 { IdePropertiesMap = GetIdeProperties(ideaBinary) } if _, ok := IdePropertiesMap[name]; ok { return IdePropertiesMap[name] } return "" } func getIdeInfoByBinary(ideaBinary string) (parameterValue IdeInfo, err error) { return getIdeInfoByPackage(getIdeIdePackageByBinary(ideaBinary)) } func GetIdeProperties(ideaBinary string) (collectedOptions map[string]string) { var ideaPackage string collectedOptions = make(map[string]string) ideaBinary, _ = DetectInstallationByInnerPath(ideaBinary, true) ideaPackage, _ = DetectInstallationByInnerPath(ideaBinary, false) InstallationInfo, _ := getIdeInfoByBinary(ideaBinary) for _, possibleIdeaPropertiesFileLocation := range getOsDependentDir(possibleIdeaPropertiesFileLocations) { possibleIdeaOptionsFile := strings.Replace(possibleIdeaPropertiesFileLocation, "{IDE_BasefileName}", strings.ToUpper(GetIdeBasefileName(ideaBinary)), -1) possibleIdeaOptionsFile = strings.Replace(possibleIdeaOptionsFile, "{dataDirectoryName}", InstallationInfo.DataDirectoryName, -1) possibleIdeaOptionsFile = strings.Replace(possibleIdeaOptionsFile, "{ideaPackage}", ideaPackage, -1) possibleIdeaOptionsFile = os.ExpandEnv(possibleIdeaOptionsFile) if FileExists(possibleIdeaOptionsFile) { //log.Println("found idea.properties file at: \"" + possibleIdeaOptionsFile + "\"") fillIdePropertiesMap(possibleIdeaOptionsFile, collectedOptions) } else { //log.Println("Checked " + possibleIdeaPropertiesFileLocation + ". There is no \"" + possibleIdeaOptionsFile + "\" file.") } } var listOfCollectedOptions string for option, value := range collectedOptions { listOfCollectedOptions = listOfCollectedOptions + option + "=" + value + "\n" } //log.Println("Collected idea properties:\n" + listOfCollectedOptions) return collectedOptions } //If any part of providedPath is IDE installation path, DetectInstallationByInnerPath returns path or binary (based on returnBinary flag) func DetectInstallationByInnerPath(providedPath string, returnBinary bool) (ideaBinary string, err error) { providedPath = filepath.Clean(providedPath) providedDeep := strings.Count(providedPath, string(os.PathSeparator)) basePath := providedPath for i := 1; i < providedDeep; i++ { if ideaBinary, err := getIdeBinaryByPackage(basePath); err == nil { if returnBinary { return ideaBinary, nil } else { return basePath, nil } } basePath = filepath.Dir(basePath) } return "", errors.New("Could not detect IDE by \"" + providedPath + "\" path") } func fillIdePropertiesMap(ideaOptionsFile string, optionsMap map[string]string) { optionsSlice, err := ideaPropertiesFileToSliceOfStrings(ideaOptionsFile) if err != nil { log.Printf("ideaPropertiesFileToSliceOfStrings failed. ideaOptionsFile: %s, error: %s", ideaOptionsFile, err) } for _, option := range optionsSlice { if idx := strings.IndexByte(option, '='); idx >= 0 { optionValue := option[idx+1:] optionValue = os.ExpandEnv(optionValue) optionName := option[:idx] if _, exist := optionsMap[optionName]; !exist { optionsMap[optionName] = optionValue } } } } func ideaPropertiesFileToSliceOfStrings(ideaPropertiesFile string) (properties []string, err error) { file, err := os.Open(ideaPropertiesFile) if err != nil { log.Printf("failed to open ideaPropertiesFile, file: %s, error: %s", ideaPropertiesFile, err) } scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) var i int for scanner.Scan() { i++ option := scanner.Text() if len(option) != 0 { if option[0] == '#' { } else { properties = append(properties, option) } } } err = file.Close() if err != nil { return nil, err } return properties, err } func GetIdeBasefileName(ideaBinary string) string { for _, possibleBaseFileName := range possibleBaseFileNames { if strings.HasSuffix(ideaBinary, possibleBaseFileName) { return possibleBaseFileName } } return "" } func UserHomeDir() string { if runtime.GOOS == "windows" { home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") if home == "" { home = os.Getenv("USERPROFILE") } return home } return os.Getenv("HOME") } func FileExists(dir string) bool { if f, err := os.Open(dir); err == nil && len(dir) > 2 { err := f.Close() if err != nil { log.Printf("Error closing file %s. Error: %s", dir, err) } return true } return false }