From 8dc7a1b85bc078aa6c4e30b5a3e62cfb27409644 Mon Sep 17 00:00:00 2001 From: esarver Date: Fri, 26 Jul 2024 09:34:06 -0400 Subject: [PATCH] Use rust side logging (#36) - [x] Save logs to expected places on all platforms - [x] Forward logs to VSCode Output Pane --- .eslintrc.yml | 3 +- CHANGELOG.md | 8 +- package-lock.json | 12 +- package.json | 4 +- src/extension.ts | 17 ++- src/instruments.ts | 20 ++- src/logging.ts | 254 ++++++++++++++++++++++++++++++++++++++ src/resourceManager.ts | 54 +++++++- src/terminationManager.ts | 19 ++- src/utility.ts | 150 ++++++++++++++++++++++ 10 files changed, 523 insertions(+), 18 deletions(-) create mode 100644 src/logging.ts create mode 100644 src/utility.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index 1b6c8dd..8bf5f16 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -40,8 +40,9 @@ settings: rules: # eslint indent: - - error + - "error" - 4 + - SwitchCase: 1 linebreak-style: - error - unix diff --git a/CHANGELOG.md b/CHANGELOG.md index bd90d0a..ba17dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how Security -- in case of vulnerabilities. --> +## [0.17.0] + +### Added +- Add logging for terminal and discover + ## [0.16.4] ### Changed @@ -123,7 +128,8 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how - Feature to retrieve TSP-Link network details -[Unreleased]: https://github.com/tektronix/tsp-toolkit/compare/v0.16.4...HEAD +[Unreleased]: https://github.com/tektronix/tsp-toolkit/compare/v0.17.0...HEAD +[0.17.0]: https://github.com/tektronix/tsp-toolkit/releases/tag/v0.16.4 [0.16.4]: https://github.com/tektronix/tsp-toolkit/releases/tag/v0.16.4 [0.16.1]: https://github.com/tektronix/tsp-toolkit/releases/tag/v0.16.1 [0.15.3]: https://github.com/tektronix/tsp-toolkit/releases/tag/v0.15.3 diff --git a/package-lock.json b/package-lock.json index 95fbe79..735a797 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "tsp-toolkit", - "version": "0.16.4", + "version": "0.17.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tsp-toolkit", - "version": "0.16.4", + "version": "0.17.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@tektronix/keithley_instrument_libraries": "0.16.0", - "@tektronix/kic-cli": "0.16.2-0", + "@tektronix/kic-cli": "0.17.0-1", "@tektronix/web-help-documents": "0.15.3", "@types/cheerio": "^0.22.35", "cheerio": "^1.0.0-rc.12", @@ -1078,9 +1078,9 @@ "integrity": "sha512-Q9KBTHtLRTqLy5rzygqa722JmFm3dpY55X6UGml3U2Wo+cwcKycCtcT8C/2qtHLxldbT598h8tZ/8rsUhXgK8g==" }, "node_modules/@tektronix/kic-cli": { - "version": "0.16.2-0", - "resolved": "https://npm.pkg.github.com/download/@tektronix/kic-cli/0.16.2-0/da72054c6e89de08180e6b81d6fc9ddcbdd2e154", - "integrity": "sha512-/1hApBgQTOzvGm8hRl5GEbz/KatGu014o8oxIux1lDdNnhVo3yO/+r1k0Rn4tx9esoQuyhVubbkSHfbQEPARtw==", + "version": "0.17.0-1", + "resolved": "https://npm.pkg.github.com/download/@tektronix/kic-cli/0.17.0-1/97674e16b7ae85ed23a3d0e0fd5b13db61978f12", + "integrity": "sha512-0fUdet3wXk6EfvPaFiDhg5AMbKz9Lt84oO+v4KpR6TS3uuEug0BtVTHzpNY4zsz27iH1HIl3OYdD2409+mE6Mw==", "bin": { "linux-kic": "bin/kic", "linux-kic-discover": "bin/kic-discover", diff --git a/package.json b/package.json index 0ffdd72..78fb30d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publisher": "Tektronix", "displayName": "[Beta] Keithley TSP Toolkit", "description": "VSCode extension for Keithley Instruments' Test Script Processor", - "version": "0.16.4", + "version": "0.17.0", "icon": "./resources/TSP_Toolkit_128x128.png", "galleryBanner": { "color": "#EEEEEE", @@ -325,7 +325,7 @@ }, "dependencies": { "@tektronix/keithley_instrument_libraries": "0.16.0", - "@tektronix/kic-cli": "0.16.2-0", + "@tektronix/kic-cli": "0.17.0-1", "@tektronix/web-help-documents": "0.15.3", "@types/cheerio": "^0.22.35", "cheerio": "^1.0.0-rc.12", diff --git a/src/extension.ts b/src/extension.ts index 0d70d10..30958ac 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -24,6 +24,8 @@ import { processWorkspaceFolders, RELATIVE_TSP_CONFIG_FILE_PATH, } from "./workspaceManager" +import { LOG_DIR } from "./utility" +import { LoggerManager } from "./logging" let _activeConnectionManager: CommunicationManager let _terminationMgr: TerminationManager @@ -454,12 +456,25 @@ async function startInstrDiscovery(): Promise { if (wait_time === undefined) { return } + const logger = LoggerManager.instance().add_logger("TSP Discovery") if (parseInt(wait_time)) { const term = vscode.window.createTerminal({ name: "Discovery", shellPath: EXECUTABLE, - shellArgs: ["discover", "all", "--timeout", wait_time], + shellArgs: [ + "--log-file", + path.join( + LOG_DIR, + `${new Date().toISOString().substring(0, 10)}-kic.log` + ), + "--log-socket", + `${logger.host}:${logger.port}`, + "discover", + "all", + "--timeout", + wait_time, + ], iconPath: vscode.Uri.file("/keithley-logo.ico"), }) term.show() diff --git a/src/instruments.ts b/src/instruments.ts index 28d8932..3fa6d4b 100644 --- a/src/instruments.ts +++ b/src/instruments.ts @@ -1,4 +1,5 @@ import * as cp from "node:child_process" +import path = require("path") import * as vscode from "vscode" import { @@ -17,6 +18,8 @@ import { IoType, KicProcessMgr, } from "./resourceManager" +import { LOG_DIR } from "./utility" +import { LoggerManager } from "./logging" const DISCOVERY_TIMEOUT = 300 @@ -1283,10 +1286,25 @@ export class InstrumentsExplorer { } private startDiscovery() { + const logger = LoggerManager.instance().add_logger("TSP Discovery") if (this.InstrumentsDiscoveryViewer.message == "") { cp.spawn( DISCOVER_EXECUTABLE, - ["all", "--timeout", DISCOVERY_TIMEOUT.toString(), "--exit"] + [ + "--log-file", + path.join( + LOG_DIR, + `${new Date() + .toISOString() + .substring(0, 10)}-kic-discover.log` + ), + "--log-socket", + `${logger.host}:${logger.port}`, + "all", + "--timeout", + DISCOVERY_TIMEOUT.toString(), + "--exit", + ] //, // { // detached: true, diff --git a/src/logging.ts b/src/logging.ts new file mode 100644 index 0000000..ed11e76 --- /dev/null +++ b/src/logging.ts @@ -0,0 +1,254 @@ +import { createServer, Server } from "net" +import { LogOutputChannel, window } from "vscode" + +const PORT_MAX = 49150 +const PORT_MIN = 48620 + +/** + * The log levels from KIC applications. + */ +enum KicLogLevels { + TRACE = "TRACE", + DEBUG = "DEBUG", + INFO = "INFO", + WARN = "WARN", + ERROR = "ERROR", +} + +/** + * This type is used in type-intersections to allow for more keys to be present in a + * type. + */ +type OtherKeys = { + [key: string]: string | number | boolean | object +} + +/** + * A span should always include a `name`, but there could be other keys that are + * included as well. This type-intersection allows us to include other keys if they + * exist. + */ +type Span = OtherKeys & { + name: string +} + +/** + * Fields will likely include a `message`, though it is not guaranteed. There could be + * other keys that are included as well. This type-intersection allows us to include + * other keys if they exist. + */ +type Fields = OtherKeys & { + message?: string +} + +/** + * KicLogMessage defines the JSON object we expect to get from the KIC application over + * the logging socket. + */ +interface KicLogMessage { + timestamp: string + level: KicLogLevels + fields: Fields + target: string + span: Span + spans: Span[] +} + +/** + * A logger will create a socket server that listens for kic log messages and prints + * them to the appropriate Output Channel. + */ +export class Logger { + private _manager?: LoggerManager + private _server: Server + private _outputChannel: LogOutputChannel + private _port: number + + readonly _name: string + readonly _host: string + + constructor( + name: string, + host: string, + port: number, + manager?: LoggerManager + ) { + this._name = name + this._host = host + this._port = port + if (manager) { + this._manager = manager + } + + this._outputChannel = window.createOutputChannel(this._name, { + log: true, + }) + + this._server = createServer((stream) => { + stream.on("data", (c) => { + //console.log(c.toString()) + const raw = "[" + c.toString() + "]" + const messages = JSON.parse(raw) as KicLogMessage[] + + for (const m of messages) { + const spans = m.spans + .map((x) => { + let span_string = "" + const { name, ...args } = x + span_string += name + if (Object.keys(args).length > 0) { + span_string += JSON.stringify(args) + } + return span_string + }) + .join(":") + + const fields = ((f: Fields): string => { + let msg_text = "" + if (f.message !== undefined) { + const { message, ...fields } = f + msg_text += message + if (Object.keys(fields).length > 0) { + msg_text = + JSON.stringify(fields) + " " + msg_text + } + } else { + if (Object.keys(f).length > 0) { + msg_text += JSON.stringify(f) + } + } + return msg_text + })(m.fields) + + const message = `${spans} ${m.target} ${fields}` + + switch (m.level) { + case KicLogLevels.TRACE: + this._outputChannel.trace(message) + break + case KicLogLevels.DEBUG: + this._outputChannel.debug(message) + break + case KicLogLevels.INFO: + this._outputChannel.info(message) + break + case KicLogLevels.WARN: + this._outputChannel.warn(message) + break + case KicLogLevels.ERROR: + this._outputChannel.error(message) + break + default: + break + } + } + }) + + stream.on("end", () => { + this._server.close() + }) + }) + + this._server.on("close", () => { + this._outputChannel.dispose() + if (this._manager) { + this._manager.remove_logger(this) + } + }) + + this._server.on("error", (e) => { + if (e.name === "EADDRINUSE") { + setTimeout(() => { + this._server.close() + this._port++ + if (this._port > PORT_MAX) { + this._outputChannel.appendLine( + "Unable to find a usable port number for logger." + ) + return + } + }, 500) + } + }) + + this._server.listen(this._port, this._host, () => { + console.log( + `Logger for ${this._name} listening on ${this._host}:${this._port}` + ) + }) + } + + get host(): string { + return this._host + } + + get port(): number { + return this._port + } +} + +/** + * A LoggerManager creates Loggers for the expected external applications (in this case + * `kic` and `kic-discover`). + */ +export class LoggerManager { + private _loggers: Map + private _next_port = PORT_MIN + + /** + * The global singleton instance of LoggerManager. + */ + private static INSTANCE: LoggerManager | undefined + + /** + * Get the singleton instance of the LoggerManager. + */ + static instance(): LoggerManager { + if (!LoggerManager.INSTANCE) { + LoggerManager.INSTANCE = new LoggerManager() + } + return LoggerManager.INSTANCE + } + + private constructor() { + this._loggers = new Map() + this.add_logger("TSP Discovery") + this.add_logger("TSP Terminal") + } + + /** + * Add a new Logger with the given name. All Loggers created here will use a host IP + * of `127.0.0.1`. The port will be incremented between each added logger, so always + * get the port number programmatically. There is no guarantees on the final port + * number of any Logger. + */ + add_logger(name: string): Logger { + if (this._loggers.has(name) && this._loggers.get(name) !== undefined) { + return this._loggers.get(name) ?? process.exit(1) // should not be able to get here because of the if statement + } + + const logger = new Logger(name, "127.0.0.1", this._next_port, this) + this._next_port = logger.port + 1 + if (this._next_port > PORT_MAX) { + this._next_port = PORT_MIN + } + this._loggers.set(name, logger) + return logger + } + + /** + * Remove a Logger by name or by object. + */ + remove_logger(logger: Logger | string) { + if (typeof logger === "string") { + this._loggers.delete(logger) + return + } + + for (const [k, v] of this._loggers) { + if (v === logger) { + this._loggers.delete(k) + } + } + } +} diff --git a/src/resourceManager.ts b/src/resourceManager.ts index 602f2f0..bbaa4ad 100644 --- a/src/resourceManager.ts +++ b/src/resourceManager.ts @@ -1,8 +1,11 @@ import * as child from "child_process" +import path = require("path") import { EventEmitter } from "events" import fetch from "node-fetch" import { EXECUTABLE } from "@tektronix/kic-cli" import * as vscode from "vscode" +import { LOG_DIR } from "./utility" +import { LoggerManager } from "./logging" export const CONNECTION_RE = /(?:([A-Za-z0-9_\-+.]*)@)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/ @@ -293,28 +296,69 @@ export class KicCell extends EventEmitter { connType, maxerr ) + const logger = LoggerManager.instance().add_logger("TSP Terminal") if (connType == "lan" && maxerr != undefined) { //getting instr info before we do the actual connection. info = child - .spawnSync(EXECUTABLE, ["info", "lan", "--json", unique_id], { - env: { CLICOLOR: "1", CLICOLOR_FORCE: "1" }, - }) + .spawnSync( + EXECUTABLE, + [ + "--log-file", + path.join( + LOG_DIR, + `${new Date() + .toISOString() + .substring(0, 10)}-kic.log` + ), + "--log-socket", + `${logger.host}:${logger.port}`, + "info", + "lan", + "--json", + unique_id, + ], + { + env: { CLICOLOR: "1", CLICOLOR_FORCE: "1" }, + } + ) .stdout.toString() if (info == "") return info this._term = vscode.window.createTerminal({ name: name, shellPath: EXECUTABLE, - shellArgs: ["connect", "lan", unique_id], + shellArgs: [ + "--log-file", + path.join( + LOG_DIR, + `${new Date().toISOString().substring(0, 10)}-kic.log` + ), + "--log-socket", + `${logger.host}:${logger.port}`, + "connect", + "lan", + unique_id, + ], iconPath: vscode.Uri.file("/keithley-logo.ico"), }) } else { this._term = vscode.window.createTerminal({ name: name, shellPath: EXECUTABLE, - shellArgs: ["connect", "usb", unique_id], + shellArgs: [ + "--log-file", + path.join( + LOG_DIR, + `${new Date().toISOString().substring(0, 10)}-kic.log` + ), + "--log-socket", + `${logger.host}:${logger.port}`, + "connect", + "usb", + unique_id, + ], iconPath: vscode.Uri.file("/keithley-logo.ico"), }) } diff --git a/src/terminationManager.ts b/src/terminationManager.ts index eb3724e..b00876a 100644 --- a/src/terminationManager.ts +++ b/src/terminationManager.ts @@ -1,6 +1,10 @@ +import path = require("node:path") import * as vscode from "vscode" import { EXECUTABLE } from "@tektronix/kic-cli" + import { CONNECTION_RE } from "./resourceManager" +import { LOG_DIR } from "./utility" +import { LoggerManager } from "./logging" export class TerminationManager { async terminateAllConn() { @@ -60,10 +64,23 @@ export class TerminationManager { const name = typeof parts[1] == "undefined" ? "KIC" : parts[1] const ip = parts[2] + const logger = LoggerManager.instance().add_logger("TSP Terminal") + const term = vscode.window.createTerminal({ name: name, shellPath: EXECUTABLE, - shellArgs: ["terminate", "lan", ip], + shellArgs: [ + "--log-file", + path.join( + LOG_DIR, + `${new Date().toISOString().substring(0, 10)}-kic.log` + ), + "--log-socket", + `${logger.host}:${logger.port}`, + "terminate", + "lan", + ip, + ], iconPath: vscode.Uri.file("/keithley-logo.ico"), }) term.show() diff --git a/src/utility.ts b/src/utility.ts new file mode 100644 index 0000000..cd037f0 --- /dev/null +++ b/src/utility.ts @@ -0,0 +1,150 @@ +import { existsSync, mkdirSync, PathLike } from "node:fs" +import { homedir, platform, tmpdir } from "node:os" +import path = require("node:path") +import process = require("node:process") + +const { env } = process + +interface PathsInterface { + data: PathLike + config: PathLike + cache: PathLike + log: PathLike + temp: PathLike +} + +class Paths { + readonly _data: string + readonly _config: string + readonly _cache: string + readonly _log: string + readonly _temp: string + + constructor(options: PathsInterface) { + this._data = options.data.toString() + this._config = options.config.toString() + this._cache = options.cache.toString() + this._log = options.log.toString() + this._temp = options.temp.toString() + } + + get data(): string { + if (!existsSync(this._data)) { + mkdirSync(this._data, { recursive: true }) + } + return this._data + } + + get config(): string { + if (!existsSync(this._config)) { + mkdirSync(this._config, { recursive: true }) + } + return this._config + } + + get cache(): string { + if (!existsSync(this._cache)) { + mkdirSync(this._cache, { recursive: true }) + } + return this._cache + } + + get log(): string { + if (!existsSync(this._log)) { + mkdirSync(this._log, { recursive: true }) + } + return this._log + } + + get temp(): string { + if (!existsSync(this._temp)) { + mkdirSync(this._temp), { recursive: true } + } + return this._temp + } +} + +// Paths referenced from https://github.com/sindresorhus/env-paths/blob/v3.0.0/index.js +// License: +// MIT License +// +// Copyright (c) Sindre Sorhus (https://sindresorhus.com) +// +// 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. + +const PATHS: Paths = (function (app_name: string): Paths { + switch (platform()) { + case "win32": { + const ad = env.APPDATA || path.join(homedir(), "AppData", "Roaming") + const lad = + env.LOCALAPPDATA || path.join(homedir(), "AppData", "Local") + + return new Paths({ + data: path.join(lad, app_name, "Data"), + config: path.join(ad, app_name, "Config"), + cache: path.join(lad, app_name, "Cache"), + log: path.join(lad, app_name, "Log"), + temp: path.join(tmpdir(), app_name), + }) + } + case "darwin": { + const lib = path.join(homedir(), "Library") + + return new Paths({ + data: path.join(lib, app_name, "Application Support"), + config: path.join(lib, app_name, "Preferences"), + cache: path.join(lib, app_name, "Caches"), + log: path.join(lib, app_name, "Logs"), + temp: path.join(tmpdir(), app_name), + }) + } + case "linux": { + const username = path.basename(homedir()) + return new Paths({ + data: path.join( + env.XDG_DATA_HOME || + path.join(homedir(), ".local", "share"), + app_name + ), + config: path.join( + env.XDG_CONFIG_HOME || path.join(homedir(), ".config"), + app_name + ), + cache: path.join( + env.XDG_CACHE_HOME || path.join(homedir(), ".cache"), + app_name + ), + log: path.join( + env.XDG_STATE_HOME || + path.join(homedir(), ".local", "state"), + app_name + ), + temp: path.join(tmpdir(), username, app_name), + }) + } + default: { + return new Paths({ + data: path.join(homedir(), app_name, "data"), + config: path.join(homedir(), app_name, "config"), + cache: path.join(homedir(), app_name, "cache"), + log: path.join(homedir(), app_name, "log"), + temp: path.join(tmpdir(), app_name), + }) + } + } +})("tsp-toolkit") + +const log_path = PATHS.log + +/** + * The appropriate location for user-level logs. + * - **Windows**: C:\Users\USERNAME\AppData\Local\tsp-toolkit\Log + * - **Linux**: ~/.local/state/tsp-toolkit + * - **macOS**: ~/Library/Logs/tsp-toolkit + */ +export const LOG_DIR = log_path +