├── assets ├── icon.png └── install_cli.py ├── .gitignore ├── src ├── child_process │ ├── handleData.js │ ├── execFileSync.js │ ├── normalizeSpawnArguments.js │ └── spawnSync.js ├── promptForApiKey.js ├── logger.js ├── onStartup.js ├── utils.js ├── sendHeartbeat.js ├── onSelectionChanged.js ├── onDocumentSaved.js ├── manifest.json └── ini.js ├── package.json ├── LICENSE ├── .appcast.xml ├── HISTORY.md └── README.md /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakatime/sketch-wakatime/HEAD/assets/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts 2 | *.sketchplugin 3 | 4 | # npm 5 | node_modules 6 | .npm 7 | npm-debug.log 8 | 9 | # mac 10 | .DS_Store 11 | 12 | # WebStorm 13 | .idea 14 | 15 | # sketch 16 | # sketch-assets 17 | -------------------------------------------------------------------------------- /src/child_process/handleData.js: -------------------------------------------------------------------------------- 1 | var Buffer = require('buffer').Buffer; 2 | 3 | function handleBuffer(buffer, encoding) { 4 | if (encoding === 'buffer') { 5 | return buffer; 6 | } 7 | if (encoding === 'NSData') { 8 | return buffer.toNSData(); 9 | } 10 | return buffer.toString(encoding); 11 | } 12 | 13 | export function handleData(data, encoding) { 14 | var buffer = Buffer.from(data); 15 | 16 | return handleBuffer(buffer, encoding); 17 | } 18 | -------------------------------------------------------------------------------- /src/promptForApiKey.js: -------------------------------------------------------------------------------- 1 | import sketch from 'sketch'; 2 | 3 | import { getConfig, setConfig } from './ini'; 4 | import { isValidApiKey } from './utils'; 5 | 6 | export default function () { 7 | let apiKey = getConfig('settings', 'api_key'); 8 | if (!isValidApiKey(apiKey)) apiKey = ''; 9 | 10 | sketch.UI.getInputFromUser( 11 | 'WakaTime API Key', 12 | { 13 | initialValue: apiKey, 14 | }, 15 | (err, value) => { 16 | if (err) return; 17 | if (!isValidApiKey(value)) return; 18 | setConfig('settings', 'api_key', value); 19 | }, 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | import { isDebugEnabled } from './ini'; 2 | 3 | export const LEVEL = { 4 | DEBUG: 'DEBUG', 5 | INFO: 'INFO', 6 | WARN: 'WARN', 7 | ERROR: 'ERROR', 8 | }; 9 | 10 | export function debug(msg) { 11 | log(msg, LEVEL.DEBUG); 12 | } 13 | 14 | export function info(msg) { 15 | log(msg, LEVEL.INFO); 16 | } 17 | 18 | export function warn(msg) { 19 | log(msg, LEVEL.WARN); 20 | } 21 | 22 | export function error(msg) { 23 | log(msg, LEVEL.ERROR); 24 | } 25 | 26 | export function log(msg, level) { 27 | if (level == LEVEL.DEBUG && !isDebugEnabled()) return; 28 | console.log('[WAKATIME] [' + level + '] ' + msg); 29 | } 30 | -------------------------------------------------------------------------------- /src/onStartup.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import child_process from '@skpm/child_process'; 3 | 4 | import promptForApiKey from './promptForApiKey'; 5 | import { getConfig } from './ini'; 6 | import { info } from './logger'; 7 | import { isValidApiKey } from './utils'; 8 | 9 | export function onStartup(context) { 10 | info('Initializing WakaTime v' + context.plugin.version()); 11 | const installScript = path.dirname(path.dirname(context.scriptPath)) + '/Resources/install_cli.py'; 12 | const bin = '/usr/bin/python3'; 13 | const args = [installScript]; 14 | child_process.execFile(bin, args); 15 | 16 | const apiKey = getConfig('settings', 'api_key'); 17 | if (!isValidApiKey(apiKey)) promptForApiKey(); 18 | } 19 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import fs from '@skpm/fs'; 2 | import os from '@skpm/os'; 3 | import process from '@skpm/process'; 4 | 5 | export function getHomeDirectory() { 6 | let home = process.env.WAKATIME_HOME; 7 | if (home && home.trim() && fs.existsSync(home.trim())) return home.trim(); 8 | return process.env['HOME'] || process.cwd(); 9 | } 10 | 11 | export function quote(str) { 12 | if (str.includes(' ')) return `"${str.replace('"', '\\"')}"`; 13 | return str; 14 | } 15 | 16 | export function isValidApiKey(key) { 17 | if (!key) return false; 18 | var re = new RegExp('^(waka_)?[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$', 'i'); 19 | return re.test(key); 20 | } 21 | 22 | export function urlToPath(path) { 23 | path = decodeURIComponent(path.toString()); 24 | if (path.indexOf('file://') == 0) path = path.substring(7); 25 | return path; 26 | } 27 | -------------------------------------------------------------------------------- /src/sendHeartbeat.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import process from '@skpm/process'; 3 | 4 | import { debug, error } from './logger'; 5 | import { execFileSync } from './child_process/execFileSync'; 6 | import { getHomeDirectory } from './utils'; 7 | 8 | export function sendHeartbeat(context, file, isWrite) { 9 | file = decodeURI(file); 10 | debug('Sending heartbeat: ' + file); 11 | const sketchVersion = NSBundle.mainBundle().infoDictionary().CFBundleShortVersionString; 12 | const bin = getHomeDirectory() + '/.wakatime/wakatime-cli'; 13 | const args = ['--entity', file, '--plugin', 'sketch/' + sketchVersion + ' sketch-wakatime/' + context.plugin.version()]; 14 | if (isWrite) args.push('--write'); 15 | 16 | debug(bin + ' ' + args.join(' ')); 17 | try { 18 | const stdout = execFileSync(bin, args); 19 | if (stdout) debug(stdout); 20 | } catch (e) { 21 | error(e); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WakaTime", 3 | "description": "", 4 | "version": "4.0.4", 5 | "engines": { 6 | "sketch": ">=3.0" 7 | }, 8 | "skpm": { 9 | "name": "WakaTime", 10 | "manifest": "src/manifest.json", 11 | "main": "WakaTime.sketchplugin", 12 | "assets": [ 13 | "assets/**/*" 14 | ] 15 | }, 16 | "scripts": { 17 | "build": "skpm-build", 18 | "watch": "skpm-build --watch", 19 | "start": "skpm-build --watch --run", 20 | "postinstall": "npm run build && skpm-link" 21 | }, 22 | "devDependencies": { 23 | "@skpm/builder": "^0.8.0" 24 | }, 25 | "dependencies": { 26 | "@skpm/child_process": "^0.4.2", 27 | "@skpm/fs": "^0.2.6", 28 | "@skpm/process": "^0.1.5", 29 | "@skpm/os": "^0.1.1" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/wakatime/sketch-wakatime.git" 34 | }, 35 | "author": "WakaTime " 36 | } 37 | -------------------------------------------------------------------------------- /src/onSelectionChanged.js: -------------------------------------------------------------------------------- 1 | import { getSelectedDocument } from 'sketch/dom'; 2 | 3 | import { debug } from './logger'; 4 | import { sendHeartbeat } from './sendHeartbeat'; 5 | 6 | export function onSelectionChanged(context) { 7 | debug('onSelectionChanged'); 8 | 9 | const document = getSelectedDocument(); 10 | if (!document) return; 11 | const currentFile = document.path; 12 | if (!currentFile) return; 13 | 14 | const seconds = 120; 15 | const now = new Date().getTime() / 1000; 16 | 17 | let threadDictionary = NSThread.mainThread().threadDictionary(); 18 | 19 | const lastTime = threadDictionary.wakatimeLastTime || now - seconds; 20 | const lastFile = threadDictionary.wakatimeLastFile; 21 | 22 | const timeElapsed = now - lastTime; 23 | if (lastFile != currentFile || timeElapsed > seconds) { 24 | threadDictionary.wakatimeLastFile = currentFile; 25 | threadDictionary.wakatimeLastTime = now; 26 | sendHeartbeat(context, currentFile, false); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/onDocumentSaved.js: -------------------------------------------------------------------------------- 1 | import { getSelectedDocument } from 'sketch/dom'; 2 | 3 | import { debug } from './logger'; 4 | import { sendHeartbeat } from './sendHeartbeat'; 5 | 6 | export function onDocumentSaved(context) { 7 | if (context.actionContext.autosaved == 1) return; 8 | debug('onDocumentSaved'); 9 | 10 | const document = getSelectedDocument(); 11 | if (!document) return; 12 | const currentFile = document.path; 13 | if (!currentFile) return; 14 | 15 | const seconds = 60; 16 | const now = new Date().getTime() / 1000; 17 | 18 | let threadDictionary = NSThread.mainThread().threadDictionary(); 19 | 20 | const lastTime = threadDictionary.wakatimeLastTime || now - seconds; 21 | const lastFile = threadDictionary.wakatimeLastFileSaved; 22 | 23 | const timeElapsed = now - lastTime; 24 | if (lastFile != currentFile || timeElapsed > seconds) { 25 | threadDictionary.wakatimeLastFile = currentFile; 26 | threadDictionary.wakatimeLastFileSaved = currentFile; 27 | threadDictionary.wakatimeLastTime = now; 28 | sendHeartbeat(context, currentFile, true); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sketch-hq/SketchAPI/develop/docs/sketch-plugin-manifest-schema.json", 3 | "icon": "icon.png", 4 | "identifier" : "com.wakatime.sketch.plugin", 5 | "description" : "Time tracking and metrics automatically generated from your Sketch usage.", 6 | "scope": "application", 7 | "compatibleVersion": 53, 8 | "commands": [ 9 | { 10 | "name": "API Key", 11 | "identifier": "wakatime.apikey", 12 | "script": "./promptForApiKey.js" 13 | }, 14 | { 15 | "name": "Selection Changed", 16 | "identifier": "wakatime.selection-changed", 17 | "script": "./onSelectionChanged.js", 18 | "handlers": { 19 | "actions": { 20 | "SelectionChanged.finish": "onSelectionChanged" 21 | } 22 | } 23 | }, 24 | { 25 | "name": "Document Saved", 26 | "identifier": "wakatime.document-saved", 27 | "script": "./onDocumentSaved.js", 28 | "handlers": { 29 | "actions": { 30 | "DocumentSaved": "onDocumentSaved" 31 | } 32 | } 33 | }, 34 | { 35 | "name": "Startup", 36 | "identifier": "wakatime.startup", 37 | "script": "./onStartup.js", 38 | "handlers": { 39 | "actions": { 40 | "Startup": "onStartup" 41 | } 42 | } 43 | } 44 | ], 45 | "menu": { 46 | "title": "WakaTime", 47 | "items": [ 48 | "wakatime.apikey" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023 Alan Hamlett. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer 13 | in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the names of WakaTime, nor the names of its 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 21 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 22 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER 24 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /src/child_process/execFileSync.js: -------------------------------------------------------------------------------- 1 | import { spawnSync } from './spawnSync'; 2 | 3 | function validateTimeout(timeout) { 4 | if (timeout != null && !(Number.isInteger(timeout) && timeout >= 0)) { 5 | throw new Error('ERR_OUT_OF_RANGE options.timeout'); 6 | } 7 | } 8 | 9 | function validateMaxBuffer(maxBuffer) { 10 | if (maxBuffer != null && !(typeof maxBuffer === 'number' && maxBuffer >= 0)) { 11 | throw new Error('ERR_OUT_OF_RANGE options.maxBuffer'); 12 | } 13 | } 14 | 15 | export function execFileSync(file, args, options) { 16 | var defaultOptions = { 17 | encoding: 'utf8', 18 | timeout: 0, 19 | maxBuffer: 200 * 1024, 20 | killSignal: 'SIGTERM', 21 | cwd: null, 22 | env: null, 23 | shell: false, 24 | }; 25 | 26 | if (typeof args === 'object' && !Array.isArray(args)) { 27 | // function (file, options) 28 | options = Object.assign(defaultOptions, args); 29 | args = []; 30 | } else { 31 | // function (file) 32 | options = Object.assign(defaultOptions, options || {}); 33 | } 34 | 35 | // Validate the timeout, if present. 36 | validateTimeout(options.timeout); 37 | 38 | // Validate maxBuffer, if present. 39 | validateMaxBuffer(options.maxBuffer); 40 | 41 | var child = spawnSync(file, args, { 42 | cwd: options.cwd, 43 | env: options.env, 44 | gid: options.gid, 45 | uid: options.uid, 46 | shell: options.shell, 47 | encoding: options.encoding, 48 | stdio: ['pipe', 'pipe', 'inherit'], 49 | }); 50 | 51 | if (child.status !== 0) { 52 | let error = new Error(`Failed to run ${child.status}: ${child.stderr}${child.stdout}`); 53 | error.pid = child.pid; 54 | error.status = child.status; 55 | error.stdout = child.stdout; 56 | error.stderr = child.stderr; 57 | throw error; 58 | } 59 | 60 | return child.stdout; 61 | } 62 | -------------------------------------------------------------------------------- /.appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | 5 | ## 4.0.4 (2023-06-30) 6 | 7 | - Url decode file paths before sending to wakatime-cli. 8 | 9 | 10 | ## 4.0.3 (2023-06-30) 11 | 12 | - Fix entity argument typo. 13 | 14 | 15 | ## 4.0.2 (2023-06-29) 16 | 17 | - Only log wakatime-cli output when not empty. 18 | 19 | 20 | ## 4.0.1 (2023-06-29) 21 | 22 | - Output errors from wakatime-cli to Console.app. 23 | 24 | 25 | ## 4.0.0 (2023-06-09) 26 | 27 | - Use new JavaScript plugin structure. 28 | 29 | 30 | ## 3.0.3 (2023-03-04) 31 | 32 | - First try installing wakatime-cli using Python3, then fallback to Python2. 33 | [#9](https://github.com/wakatime/sketch-wakatime/issues/9) 34 | 35 | 36 | ## 3.0.2 (2022-11-27) 37 | 38 | - Support api key with waka prefix. 39 | - Only remove wakatime-cli after finished downloading new one. 40 | - Improve request handling when installing wakatime-cli. 41 | 42 | 43 | ## 3.0.1 (2021-12-23) 44 | 45 | - Fix executing wakatime-cli. 46 | 47 | 48 | ## 3.0.0 (2021-12-23) 49 | 50 | - Use new Go wakatime-cli from GitHub Releases, with auto-updating. 51 | 52 | 53 | ## 2.0.0 (2018-12-19) 54 | 55 | - Upgrade wakatime-cli to v10.6.1. 56 | 57 | 58 | ## 1.0.11 (2017-06-01) 59 | 60 | - Support automatic plugin updates using Sketch Appcast updates. 61 | 62 | 63 | ## 1.0.10 (2017-05-24) 64 | 65 | - Upgrade wakatime-cli to v8.0.2. 66 | 67 | 68 | ## 1.0.9 (2017-02-20) 69 | 70 | - Upgrade wakatime-cli to v7.0.2. 71 | 72 | 73 | ## 1.0.8 (2016-10-24) 74 | 75 | - Upgrade wakatime-cli to v6.2.0. 76 | 77 | 78 | ## 1.0.7 (2016-07-06) 79 | 80 | - Upgrade wakatime-cli to v6.0.7. 81 | 82 | 83 | ## 1.0.6 (2016-06-17) 84 | 85 | - URL decode currently open file into path. 86 | - Upgrade wakatime-cli to v6.0.6. 87 | 88 | 89 | ## 1.0.5 (2016-06-15) 90 | 91 | - Log verbose messages to System Console app when debug is true. 92 | - Upgrade wakatime-cli to v6.0.5. 93 | 94 | 95 | ## 1.0.4 (2016-06-09) 96 | 97 | - Upgrade wakatime-cli to v6.0.4 to fix bug in urllib3 package causing 98 | unhandled retry exceptions. 99 | 100 | 101 | ## 1.0.3 (2016-06-09) 102 | 103 | - Improve performance by keeping plugin around until Sketch app quits, and 104 | storing state in memory instead of NSUserDefaults. 105 | 106 | 107 | ## 1.0.2 (2016-06-08) 108 | 109 | - Improve performance by only checking for api key on startup. 110 | 111 | 112 | ## 1.0.1 (2016-06-07) 113 | 114 | - URLDecode spaces from Application Support directory path. 115 | 116 | 117 | ## 1.0.0 (2016-06-07) 118 | 119 | - Birth 120 | 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sketch-wakatime 2 | 3 | [![Coding time tracker](https://wakatime.com/badge/github/wakatime/sketch-wakatime.svg)](https://wakatime.com/badge/github/wakatime/sketch-wakatime) 4 | 5 | Time tracking and metrics automatically generated from your [Sketch](http://www.sketchapp.com/) usage. 6 | 7 | ## Installation 8 | 9 | 1. Download the [latest release](https://github.com/wakatime/sketch-wakatime/releases/latest). 10 | 11 | 2. Unzip the file. 12 | 13 | 3. Open the `WakaTime.sketchplugin` file to install the plugin. 14 | 15 | 4. Use Sketch like you normally do and your time will automatically be tracked for you. 16 | 17 | 5. Enter your [api key](https://wakatime.com/settings#apikey) if prompted. 18 | 19 | 6. Visit to see your logged time. 20 | 21 | ## Screen Shots 22 | 23 | ![Project Overview](https://wakatime.com/static/img/ScreenShots/Screen-Shot-2016-03-21.png) 24 | 25 | ## Configuring 26 | 27 | To change your api key, copy it from your [Settings page](https://wakatime.com/settings#apikey), then paste into Sketch `Plugins → WakaTime`. 28 | 29 | Additional settings are in `$HOME/.wakatime.cfg` for [wakatime cli](https://github.com/wakatime/wakatime#configuring). 30 | 31 | ## Contributing 32 | 33 | To run this plugin from a local clone of the repo: 34 | 35 | 1. `git clone git@github.com:wakatime/sketch-wakatime.git` 36 | 2. `cd sketch-wakatime` 37 | 3. `ln -s "$PWD/sketch-wakatime/WakaTime.sketchplugin" ~/Library/Application\ Support/com.bohemiancoding.sketch3/Plugins/WakaTime.sketchplugin` 38 | 4. `npm run watch` 39 | 40 | To view the output from `console.log`, you have a few different options: 41 | 42 | - Use the [`sketch-dev-tools`](https://github.com/skpm/sketch-dev-tools) 43 | - Run `skpm log` in your Terminal, with the optional `-f` argument (`skpm log -f`) which causes `skpm log` to not stop when the end of logs is reached, but rather to wait for additional data to be appended to the input 44 | 45 | ### Publishing 46 | 47 | ```bash 48 | skpm publish 49 | ``` 50 | 51 | (where `bump` can be `patch`, `minor` or `major`) 52 | 53 | `skpm publish` will create a new release on your GitHub repository and create an appcast file in order for Sketch users to be notified of the update. 54 | 55 | ## Troubleshooting 56 | 57 | The Sketch plugin logs errors to `Console.app` and `~/.wakatime/wakatime.log`. 58 | 59 | For more info on debugging Sketch plugins see the [official docs](https://developer.sketch.com/plugins/debugging). 60 | 61 | For more general troubleshooting information, see [wakatime/wakatime#troubleshooting](https://github.com/wakatime/wakatime#troubleshooting). 62 | -------------------------------------------------------------------------------- /src/ini.js: -------------------------------------------------------------------------------- 1 | import fs from '@skpm/fs'; 2 | 3 | import { getHomeDirectory } from './utils'; 4 | 5 | export function getConfig(section, key) { 6 | const file = getHomeDirectory() + '/.wakatime.cfg'; 7 | let contents = ''; 8 | try { 9 | contents = fs.readFileSync(file, { encoding: 'utf-8' }); 10 | } catch (e) {} 11 | if (contents) { 12 | let currentSection = ''; 13 | let lines = contents.split('\n'); 14 | for (let i in lines) { 15 | let line = lines[i]; 16 | if (line) { 17 | if (line.trim().indexOf('[') == 0 && line.trim().indexOf(']') == line.length - 1) { 18 | currentSection = line 19 | .trim() 20 | .substring(1, line.trim().length - 1) 21 | .toLowerCase(); 22 | } else if (section.toLowerCase() == currentSection) { 23 | const parts = line.split('='); 24 | if (parts.length == 2 && parts[0].trim() == key) { 25 | return parts[1].trim(); 26 | } 27 | } 28 | } 29 | } 30 | } 31 | return null; 32 | } 33 | 34 | export function setConfig(section, key, val) { 35 | const file = getHomeDirectory() + '/.wakatime.cfg'; 36 | let output = []; 37 | let currentSection = ''; 38 | let found = false; 39 | let contents = ''; 40 | try { 41 | contents = fs.readFileSync(file, { encoding: 'utf-8' }); 42 | } catch (e) {} 43 | if (contents) { 44 | let lines = contents.split('\n'); 45 | for (let i in lines) { 46 | const line = lines[i]; 47 | if (line && line.trim().indexOf('[') == 0 && line.trim().indexOf(']') == line.length - 1) { 48 | if (section.toLowerCase() == currentSection && !found) { 49 | output.push(key + ' = ' + val); 50 | found = true; 51 | } 52 | currentSection = line 53 | .trim() 54 | .substring(1, line.trim().length - 1) 55 | .toLowerCase(); 56 | output.push(line); 57 | } else if (line && section.toLowerCase() == currentSection) { 58 | const parts = line.split('='); 59 | const currentKey = parts[0].trim(); 60 | if (currentKey == key) { 61 | if (!found) { 62 | output.push(key + ' = ' + val); 63 | found = true; 64 | } 65 | } else { 66 | output.push(line); 67 | } 68 | } else { 69 | output.push(line); 70 | } 71 | } 72 | } 73 | if (!found) { 74 | if (section.toLowerCase() != currentSection) { 75 | output.push('[' + section.toLowerCase() + ']'); 76 | } 77 | output.push(key + ' = ' + val); 78 | } 79 | fs.writeFileSync(file, output.join('\n'), { encoding: 'utf8' }); 80 | } 81 | 82 | export function isDebugEnabled() { 83 | let threadDictionary = NSThread.mainThread().threadDictionary(); 84 | if (threadDictionary.wakatimeDebug === true || threadDictionary.wakatimeDebug === false) return threadDictionary.wakatimeDebug; 85 | 86 | const debug = getConfig('settings', 'debug'); 87 | threadDictionary.wakatimeDebug = debug === 'true'; 88 | return threadDictionary.wakatimeDebug; 89 | } 90 | -------------------------------------------------------------------------------- /src/child_process/normalizeSpawnArguments.js: -------------------------------------------------------------------------------- 1 | export function normalizeSpawnArguments(file, args, options) { 2 | if (typeof file !== 'string' || file.length === 0) { 3 | throw new Error('ERR_INVALID_ARG_TYPE') 4 | } 5 | 6 | if (Array.isArray(args)) { 7 | args = args.slice(0) 8 | } else if ( 9 | args !== undefined && 10 | (args === null || typeof args !== 'object') 11 | ) { 12 | throw new Error('ERR_INVALID_ARG_TYPE args') 13 | } else { 14 | options = args 15 | args = [] 16 | } 17 | 18 | if (options === undefined) { 19 | options = {} 20 | } else if (options === null || typeof options !== 'object') { 21 | throw new Error('ERR_INVALID_ARG_TYPE options') 22 | } 23 | 24 | // Validate the cwd, if present. 25 | if (options.cwd != null && typeof options.cwd !== 'string') { 26 | throw new Error('ERR_INVALID_ARG_TYPE options.cwd') 27 | } 28 | 29 | // Validate detached, if present. 30 | if (options.detached != null && typeof options.detached !== 'boolean') { 31 | throw new Error('ERR_INVALID_ARG_TYPE options.detached') 32 | } 33 | 34 | // Validate the uid, if present. 35 | if (options.uid != null && !Number.isInteger(options.uid)) { 36 | throw new Error('ERR_INVALID_ARG_TYPE options.uid') 37 | } 38 | 39 | // Validate the gid, if present. 40 | if (options.gid != null && !Number.isInteger(options.gid)) { 41 | throw new Error('ERR_INVALID_ARG_TYPE options.gid') 42 | } 43 | 44 | // Validate the shell, if present. 45 | if ( 46 | options.shell != null && 47 | typeof options.shell !== 'boolean' && 48 | typeof options.shell !== 'string' 49 | ) { 50 | throw new Error('ERR_INVALID_ARG_TYPE options.shell') 51 | } 52 | 53 | // Validate argv0, if present. 54 | if (options.argv0 != null && typeof options.argv0 !== 'string') { 55 | throw new Error('ERR_INVALID_ARG_TYPE options.argv0') 56 | } 57 | 58 | // Make a shallow copy so we don't clobber the user's options object. 59 | options = Object.assign({}, options) 60 | 61 | if (options.shell) { 62 | var command = [file].concat(args).join(' ') 63 | 64 | if (typeof options.shell === 'string') { 65 | file = options.shell 66 | } else { 67 | file = '/bin/bash' 68 | } 69 | args = ['-l', '-c', command] 70 | } 71 | 72 | if (typeof options.argv0 === 'string') { 73 | args.unshift(options.argv0) 74 | } 75 | 76 | var stdio = ['pipe', 'pipe', 'pipe'] 77 | 78 | if (typeof options.stdio === 'string') { 79 | if (options.stdio === 'inherit') { 80 | stdio = [0, 1, 2] 81 | } else { 82 | stdio = [options.stdio, options.stdio, options.stdio] 83 | } 84 | } else if (Array.isArray(options.stdio)) { 85 | if (options.stdio[0] || options.stdio[0] === 0) { 86 | if (options.stdio[0] === 'inherit') { 87 | stdio[0] = 0 88 | } else { 89 | stdio[0] = options.stdio[0] 90 | } 91 | } 92 | if (options.stdio[1] || options.stdio[1] === 0) { 93 | if (options.stdio[1] === 'inherit') { 94 | stdio[1] = 1 95 | } else { 96 | stdio[1] = options.stdio[1] 97 | } 98 | } 99 | if (options.stdio[2] || options.stdio[2] === 0) { 100 | if (options.stdio[2] === 'inherit') { 101 | stdio[2] = 2 102 | } else { 103 | stdio[2] = options.stdio[2] 104 | } 105 | } 106 | } 107 | 108 | var env = options.env 109 | 110 | return { 111 | file: file, 112 | args: args, 113 | options: options, 114 | envPairs: env, 115 | stdio: stdio 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/child_process/spawnSync.js: -------------------------------------------------------------------------------- 1 | import { normalizeSpawnArguments } from './normalizeSpawnArguments'; 2 | import { handleData } from './handleData'; 3 | 4 | export function spawnSync(_command, _args, _options) { 5 | var opts = normalizeSpawnArguments(_command, _args, _options); 6 | 7 | if (opts.file[0] !== '.' && opts.file[0] !== '/' && opts.file[0] !== '~') { 8 | // means that someone refered to an executable that might be in the path, let's find it 9 | var whichChild = spawnSync('/bin/bash', ['-l', '-c', 'which ' + opts.file], { encoding: 'utf8' }); 10 | if (whichChild.err) { 11 | return whichChild; 12 | } 13 | var resolvedCommand = String(whichChild.stdout).trim(); 14 | if (!resolvedCommand.length) { 15 | return { 16 | err: new Error(String(opts.file) + ' ENOENT'), 17 | }; 18 | } 19 | return spawnSync(resolvedCommand, _args, _options); 20 | } 21 | 22 | var options = opts.options; 23 | 24 | var pipe = NSPipe.pipe(); 25 | var errPipe = NSPipe.pipe(); 26 | 27 | try { 28 | var task = NSTask.alloc().init(); 29 | task.setLaunchPath(NSString.stringWithString(opts.file).stringByExpandingTildeInPath()); 30 | task.arguments = NSArray.arrayWithArray(opts.args || []); 31 | if (opts.envPairs) { 32 | task.environment = opts.envPairs; 33 | } 34 | 35 | if (options.cwd) { 36 | task.setCurrentDirectoryPath(NSString.stringWithString(options.cwd).stringByExpandingTildeInPath()); 37 | } 38 | 39 | task.setStandardOutput(pipe); 40 | task.setStandardError(errPipe); 41 | 42 | task.launch(); 43 | task.waitUntilExit(); 44 | 45 | var stdoutIgnored = false; 46 | var stderrIgnored = false; 47 | 48 | var data; 49 | var stdoutValue; 50 | var stderrValue; 51 | 52 | if (opts.stdio[1] === 'ignored') { 53 | stdoutIgnored = true; 54 | } else if (opts.stdio[1] === 1) { 55 | data = pipe.fileHandleForReading().readDataToEndOfFile(); 56 | stdoutValue = handleData(data, options.encoding || 'utf8'); 57 | } else if (opts.stdio[1] === 2) { 58 | data = pipe.fileHandleForReading().readDataToEndOfFile(); 59 | stdoutValue = handleData(data, options.encoding || 'utf8'); 60 | } 61 | 62 | if (opts.stdio[2] === 'ignored') { 63 | stderrIgnored = true; 64 | } else if (opts.stdio[2] === 1) { 65 | data = errPipe.fileHandleForReading().readDataToEndOfFile(); 66 | stderrValue = handleData(data, options.encoding || 'utf8'); 67 | } else if (opts.stdio[2] === 2) { 68 | data = errPipe.fileHandleForReading().readDataToEndOfFile(); 69 | stderrValue = handleData(data, options.encoding || 'utf8'); 70 | } 71 | 72 | let stdout = null; 73 | if (!stdoutIgnored) { 74 | if (stdoutValue) { 75 | stdout = stdoutValue; 76 | } else { 77 | data = pipe.fileHandleForReading().readDataToEndOfFile(); 78 | stdout = handleData(data, options.encoding || 'utf8'); 79 | } 80 | } 81 | 82 | let stderr = null; 83 | if (!stderrIgnored) { 84 | if (stderrValue) { 85 | stderr = stderrValue; 86 | } else { 87 | data = errPipe.fileHandleForReading().readDataToEndOfFile(); 88 | stderr = handleData(data, options.encoding || 'utf8'); 89 | } 90 | } 91 | 92 | return { 93 | pid: String(task.processIdentifier()), 94 | status: Number(task.terminationStatus()), 95 | stdout: stdout, 96 | stderr: stderr, 97 | }; 98 | } catch (err) { 99 | return { 100 | err: err, 101 | }; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /assets/install_cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import contextlib 5 | import json 6 | import os 7 | import platform 8 | import re 9 | import shutil 10 | import ssl 11 | import subprocess 12 | import sys 13 | import traceback 14 | from subprocess import PIPE 15 | from zipfile import ZipFile 16 | 17 | try: 18 | from ConfigParser import SafeConfigParser as ConfigParser 19 | from ConfigParser import Error as ConfigParserError 20 | except ImportError: 21 | from configparser import ConfigParser, Error as ConfigParserError 22 | try: 23 | from urllib2 import Request, urlopen, HTTPError 24 | except ImportError: 25 | from urllib.request import Request, urlopen 26 | from urllib.error import HTTPError 27 | 28 | 29 | def getOsName(): 30 | os = platform.system().lower() 31 | if os.startswith('cygwin') or os.startswith('mingw') or os.startswith('msys'): 32 | return 'windows' 33 | return os 34 | 35 | 36 | GITHUB_RELEASES_STABLE_URL = 'https://api.github.com/repos/wakatime/wakatime-cli/releases/latest' 37 | GITHUB_DOWNLOAD_PREFIX = 'https://github.com/wakatime/wakatime-cli/releases/download' 38 | PLUGIN = 'sketch' 39 | 40 | is_py2 = (sys.version_info[0] == 2) 41 | is_py3 = (sys.version_info[0] == 3) 42 | is_win = getOsName() == 'windows' 43 | 44 | HOME_FOLDER = None 45 | CONFIGS = None 46 | INTERNAL_CONFIGS = None 47 | 48 | 49 | def main(home=None): 50 | global CONFIGS, HOME_FOLDER 51 | 52 | if home: 53 | HOME_FOLDER = home 54 | 55 | CONFIGS = parseConfigFile(getConfigFile()) 56 | 57 | if os.path.exists(os.path.join(getHomeFolder(), '.wakatime-internal.cfg')): 58 | try: 59 | os.remove(os.path.join(getHomeFolder(), '.wakatime-internal.cfg')) 60 | except: 61 | log(traceback.format_exc()) 62 | 63 | if not os.path.exists(getResourcesFolder()): 64 | os.makedirs(getResourcesFolder()) 65 | 66 | if not isCliLatest(): 67 | downloadCLI() 68 | 69 | 70 | if is_py2: 71 | import codecs 72 | open = codecs.open 73 | 74 | def u(text): 75 | if text is None: 76 | return None 77 | if isinstance(text, unicode): # noqa: F821 78 | return text 79 | try: 80 | return text.decode('utf-8') 81 | except: 82 | try: 83 | return text.decode(sys.getdefaultencoding()) 84 | except: 85 | try: 86 | return unicode(text) # noqa: F821 87 | except: 88 | try: 89 | return text.decode('utf-8', 'replace') 90 | except: 91 | try: 92 | return unicode(str(text)) # noqa: F821 93 | except: 94 | return unicode('') # noqa: F821 95 | 96 | elif is_py3: 97 | def u(text): 98 | if text is None: 99 | return None 100 | if isinstance(text, bytes): 101 | try: 102 | return text.decode('utf-8') 103 | except: 104 | try: 105 | return text.decode(sys.getdefaultencoding()) 106 | except: 107 | pass 108 | try: 109 | return str(text) 110 | except: 111 | return text.decode('utf-8', 'replace') 112 | 113 | else: 114 | raise Exception('Unsupported Python version: {0}.{1}.{2}'.format( 115 | sys.version_info[0], 116 | sys.version_info[1], 117 | sys.version_info[2], 118 | )) 119 | 120 | 121 | class Popen(subprocess.Popen): 122 | """Patched Popen to prevent opening cmd window on Windows platform.""" 123 | 124 | def __init__(self, *args, **kwargs): 125 | if is_win: 126 | startupinfo = kwargs.get('startupinfo') 127 | try: 128 | startupinfo = startupinfo or subprocess.STARTUPINFO() 129 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 130 | except AttributeError: 131 | pass 132 | kwargs['startupinfo'] = startupinfo 133 | super(Popen, self).__init__(*args, **kwargs) 134 | 135 | 136 | def parseConfigFile(configFile): 137 | """Returns a configparser.SafeConfigParser instance with configs 138 | read from the config file. Default location of the config file is 139 | at ~/.wakatime.cfg. 140 | """ 141 | 142 | kwargs = {} if is_py2 else {'strict': False} 143 | configs = ConfigParser(**kwargs) 144 | try: 145 | with open(configFile, 'r', encoding='utf-8') as fh: 146 | try: 147 | if is_py2: 148 | configs.readfp(fh) 149 | else: 150 | configs.read_file(fh) 151 | return configs 152 | except ConfigParserError: 153 | print(traceback.format_exc()) 154 | return configs 155 | except IOError: 156 | return configs 157 | 158 | 159 | def log(message, *args, **kwargs): 160 | if not CONFIGS.has_option('settings', 'debug') or CONFIGS.get('settings', 'debug') != 'true': 161 | return 162 | msg = message 163 | if len(args) > 0: 164 | msg = message.format(*args) 165 | elif len(kwargs) > 0: 166 | msg = message.format(**kwargs) 167 | try: 168 | print('[WakaTime Install] {msg}'.format(msg=msg)) 169 | except UnicodeDecodeError: 170 | print(u('[WakaTime Install] {msg}').format(msg=u(msg))) 171 | 172 | 173 | def getHomeFolder(): 174 | global HOME_FOLDER 175 | 176 | if not HOME_FOLDER: 177 | if len(sys.argv) == 2: 178 | HOME_FOLDER = sys.argv[-1] 179 | else: 180 | HOME_FOLDER = os.path.realpath(os.environ.get('WAKATIME_HOME') or os.path.expanduser('~')) 181 | 182 | return HOME_FOLDER 183 | 184 | 185 | def getResourcesFolder(): 186 | return os.path.join(getHomeFolder(), '.wakatime') 187 | 188 | 189 | def getConfigFile(internal=None): 190 | if internal: 191 | return os.path.join(getResourcesFolder(), 'wakatime-internal.cfg') 192 | return os.path.join(getHomeFolder(), '.wakatime.cfg') 193 | 194 | 195 | def downloadCLI(): 196 | log('Downloading wakatime-cli...') 197 | 198 | if os.path.isdir(os.path.join(getResourcesFolder(), 'wakatime-cli')): 199 | shutil.rmtree(os.path.join(getResourcesFolder(), 'wakatime-cli')) 200 | 201 | try: 202 | url = cliDownloadUrl() 203 | log('Downloading wakatime-cli from {url}'.format(url=url)) 204 | zip_file = os.path.join(getResourcesFolder(), 'wakatime-cli.zip') 205 | download(url, zip_file) 206 | 207 | if isCliInstalled(): 208 | try: 209 | os.remove(getCliLocation()) 210 | except: 211 | log(traceback.format_exc()) 212 | 213 | log('Extracting wakatime-cli...') 214 | with contextlib.closing(ZipFile(zip_file)) as zf: 215 | zf.extractall(getResourcesFolder()) 216 | 217 | if not is_win: 218 | os.chmod(getCliLocation(), 509) # 755 219 | 220 | try: 221 | os.remove(os.path.join(getResourcesFolder(), 'wakatime-cli.zip')) 222 | except: 223 | log(traceback.format_exc()) 224 | except: 225 | log(traceback.format_exc()) 226 | 227 | createSymlink() 228 | 229 | log('Finished extracting wakatime-cli.') 230 | 231 | 232 | WAKATIME_CLI_LOCATION = None 233 | 234 | 235 | def getCliLocation(): 236 | global WAKATIME_CLI_LOCATION 237 | 238 | if not WAKATIME_CLI_LOCATION: 239 | binary = 'wakatime-cli-{osname}-{arch}{ext}'.format( 240 | osname=getOsName(), 241 | arch=architecture(), 242 | ext='.exe' if is_win else '', 243 | ) 244 | WAKATIME_CLI_LOCATION = os.path.join(getResourcesFolder(), binary) 245 | 246 | return WAKATIME_CLI_LOCATION 247 | 248 | 249 | def architecture(): 250 | arch = platform.machine() or platform.processor() 251 | if arch == 'armv7l': 252 | return 'arm' 253 | if arch == 'aarch64': 254 | return 'arm64' 255 | if 'arm' in arch: 256 | return 'arm64' if sys.maxsize > 2**32 else 'arm' 257 | return 'amd64' if sys.maxsize > 2**32 else '386' 258 | 259 | 260 | def isCliInstalled(): 261 | return os.path.exists(getCliLocation()) 262 | 263 | 264 | def isCliLatest(): 265 | if not isCliInstalled(): 266 | return False 267 | 268 | args = [getCliLocation(), '--version'] 269 | try: 270 | stdout, stderr = Popen(args, stdout=PIPE, stderr=PIPE).communicate() 271 | except: 272 | return False 273 | stdout = (stdout or b'') + (stderr or b'') 274 | localVer = extractVersion(stdout.decode('utf-8')) 275 | if not localVer: 276 | log('Local wakatime-cli version not found.') 277 | return False 278 | if localVer == "": 279 | log('Local wakatime-cli version is , skip updating.') 280 | return True 281 | 282 | log('Current wakatime-cli version is %s' % localVer) 283 | log('Checking for updates to wakatime-cli...') 284 | 285 | remoteVer = getLatestCliVersion() 286 | 287 | if not remoteVer: 288 | return True 289 | 290 | if remoteVer == localVer: 291 | log('wakatime-cli is up to date.') 292 | return True 293 | 294 | log('Found an updated wakatime-cli %s' % remoteVer) 295 | return False 296 | 297 | 298 | LATEST_CLI_VERSION = None 299 | 300 | 301 | def getLatestCliVersion(): 302 | global LATEST_CLI_VERSION 303 | 304 | if LATEST_CLI_VERSION: 305 | return LATEST_CLI_VERSION 306 | 307 | configs, last_modified, last_version = None, None, None 308 | try: 309 | configs = parseConfigFile(getConfigFile(True)) 310 | if configs: 311 | if configs.has_option('internal', 'cli_version'): 312 | last_version = configs.get('internal', 'cli_version') 313 | if last_version and configs.has_option('internal', 'cli_version_last_modified'): 314 | last_modified = configs.get('internal', 'cli_version_last_modified') 315 | except: 316 | log(traceback.format_exc()) 317 | 318 | try: 319 | headers, contents, code = request(GITHUB_RELEASES_STABLE_URL, last_modified=last_modified) 320 | 321 | log('GitHub API Response {0}'.format(code)) 322 | 323 | if code == 304: 324 | LATEST_CLI_VERSION = last_version 325 | return last_version 326 | 327 | data = json.loads(contents.decode('utf-8')) 328 | 329 | ver = data['tag_name'] 330 | log('Latest wakatime-cli version from GitHub: {0}'.format(ver)) 331 | 332 | if configs: 333 | last_modified = headers.get('Last-Modified') 334 | if not configs.has_section('internal'): 335 | configs.add_section('internal') 336 | configs.set('internal', 'cli_version', str(u(ver))) 337 | configs.set('internal', 'cli_version_last_modified', str(u(last_modified))) 338 | with open(getConfigFile(True), 'w', encoding='utf-8') as fh: 339 | configs.write(fh) 340 | 341 | LATEST_CLI_VERSION = ver 342 | return ver 343 | except: 344 | log(traceback.format_exc()) 345 | return None 346 | 347 | 348 | def extractVersion(text): 349 | pattern = re.compile(r"([0-9]+\.[0-9]+\.[0-9]+)") 350 | match = pattern.search(text) 351 | if match: 352 | return 'v{ver}'.format(ver=match.group(1)) 353 | if text and text.strip() == "": 354 | return "" 355 | return None 356 | 357 | 358 | def cliDownloadUrl(): 359 | osname = getOsName() 360 | arch = architecture() 361 | 362 | validCombinations = [ 363 | 'darwin-amd64', 364 | 'darwin-arm64', 365 | 'freebsd-386', 366 | 'freebsd-amd64', 367 | 'freebsd-arm', 368 | 'linux-386', 369 | 'linux-amd64', 370 | 'linux-arm', 371 | 'linux-arm64', 372 | 'netbsd-386', 373 | 'netbsd-amd64', 374 | 'netbsd-arm', 375 | 'openbsd-386', 376 | 'openbsd-amd64', 377 | 'openbsd-arm', 378 | 'openbsd-arm64', 379 | 'windows-386', 380 | 'windows-amd64', 381 | 'windows-arm64', 382 | ] 383 | check = '{osname}-{arch}'.format(osname=osname, arch=arch) 384 | if check not in validCombinations: 385 | reportMissingPlatformSupport(osname, arch) 386 | 387 | version = getLatestCliVersion() 388 | 389 | return '{prefix}/{version}/wakatime-cli-{osname}-{arch}.zip'.format( 390 | prefix=GITHUB_DOWNLOAD_PREFIX, 391 | version=version, 392 | osname=osname, 393 | arch=arch, 394 | ) 395 | 396 | 397 | def reportMissingPlatformSupport(osname, arch): 398 | url = 'https://api.wakatime.com/api/v1/cli-missing?osname={osname}&architecture={arch}&plugin={plugin}'.format( 399 | osname=osname, 400 | arch=arch, 401 | plugin=PLUGIN, 402 | ) 403 | request(url) 404 | 405 | 406 | def request(url, last_modified=None): 407 | req = Request(url) 408 | req.add_header('User-Agent', 'github.com/wakatime/{plugin}-wakatime'.format(plugin=PLUGIN)) 409 | 410 | proxy = CONFIGS.get('settings', 'proxy') if CONFIGS.has_option('settings', 'proxy') else None 411 | if proxy: 412 | req.set_proxy(proxy, 'https') 413 | 414 | if last_modified: 415 | req.add_header('If-Modified-Since', last_modified) 416 | 417 | try: 418 | resp = urlopen(req) 419 | try: 420 | headers = dict(resp.getheaders()) 421 | except: 422 | headers = dict(resp.headers) 423 | return headers, resp.read(), resp.getcode() 424 | except HTTPError as err: 425 | if err.code == 304: 426 | return None, None, 304 427 | if is_py2: 428 | with SSLCertVerificationDisabled(): 429 | try: 430 | resp = urlopen(req) 431 | try: 432 | headers = dict(resp.getheaders()) 433 | except: 434 | headers = dict(resp.headers) 435 | return headers, resp.read(), resp.getcode() 436 | except HTTPError as err2: 437 | if err2.code == 304: 438 | return None, None, 304 439 | log(err.read().decode()) 440 | log(err2.read().decode()) 441 | raise 442 | except IOError: 443 | raise 444 | log(err.read().decode()) 445 | raise 446 | except IOError: 447 | if is_py2: 448 | with SSLCertVerificationDisabled(): 449 | try: 450 | resp = urlopen(url) 451 | try: 452 | headers = dict(resp.getheaders()) 453 | except: 454 | headers = dict(resp.headers) 455 | return headers, resp.read(), resp.getcode() 456 | except HTTPError as err: 457 | if err.code == 304: 458 | return None, None, 304 459 | log(err.read().decode()) 460 | raise 461 | except IOError: 462 | raise 463 | raise 464 | 465 | 466 | def download(url, filePath): 467 | req = Request(url) 468 | req.add_header('User-Agent', 'github.com/wakatime/{plugin}-wakatime'.format(plugin=PLUGIN)) 469 | 470 | proxy = CONFIGS.get('settings', 'proxy') if CONFIGS.has_option('settings', 'proxy') else None 471 | if proxy: 472 | req.set_proxy(proxy, 'https') 473 | 474 | with open(filePath, 'wb') as fh: 475 | try: 476 | resp = urlopen(req) 477 | fh.write(resp.read()) 478 | except HTTPError as err: 479 | if err.code == 304: 480 | return None, None, 304 481 | if is_py2: 482 | with SSLCertVerificationDisabled(): 483 | try: 484 | resp = urlopen(req) 485 | fh.write(resp.read()) 486 | return 487 | except HTTPError as err2: 488 | log(err.read().decode()) 489 | log(err2.read().decode()) 490 | raise 491 | except IOError: 492 | raise 493 | log(err.read().decode()) 494 | raise 495 | except IOError: 496 | if is_py2: 497 | with SSLCertVerificationDisabled(): 498 | try: 499 | resp = urlopen(url) 500 | fh.write(resp.read()) 501 | return 502 | except HTTPError as err: 503 | log(err.read().decode()) 504 | raise 505 | except IOError: 506 | raise 507 | raise 508 | 509 | 510 | def is_symlink(path): 511 | try: 512 | return os.is_symlink(path) 513 | except: 514 | return False 515 | 516 | 517 | def createSymlink(): 518 | link = os.path.join(getResourcesFolder(), 'wakatime-cli') 519 | if is_win: 520 | link = link + '.exe' 521 | elif os.path.exists(link) and is_symlink(link): 522 | return # don't re-create symlink on Unix-like platforms 523 | 524 | if os.path.isdir(link): 525 | shutil.rmtree(link) 526 | elif os.path.isfile(link): 527 | os.remove(link) 528 | 529 | try: 530 | os.symlink(getCliLocation(), link) 531 | except: 532 | try: 533 | shutil.copy2(getCliLocation(), link) 534 | if not is_win: 535 | os.chmod(link, 509) # 755 536 | except: 537 | log(traceback.format_exc()) 538 | 539 | 540 | class SSLCertVerificationDisabled(object): 541 | 542 | def __enter__(self): 543 | self.original_context = ssl._create_default_https_context 544 | ssl._create_default_https_context = ssl._create_unverified_context 545 | 546 | def __exit__(self, *args, **kwargs): 547 | ssl._create_default_https_context = self.original_context 548 | 549 | 550 | if __name__ == '__main__': 551 | main() 552 | sys.exit(0) 553 | --------------------------------------------------------------------------------