diff --git a/client/platform/desktop/backend/ipcService.ts b/client/platform/desktop/backend/ipcService.ts index bc05a8c84..e5d1fa9b2 100644 --- a/client/platform/desktop/backend/ipcService.ts +++ b/client/platform/desktop/backend/ipcService.ts @@ -1,9 +1,11 @@ -import { ipcMain } from 'electron'; +import OS from 'os'; +import { ipcMain } from 'electron'; import { DesktopJobUpdate, RunPipeline, Settings } from '../constants'; import server from './server'; import linux from './platforms/linux'; +import win32 from './platforms/windows'; import common from './platforms/common'; export default function register() { @@ -16,10 +18,6 @@ export default function register() { * Platform-agnostic methods */ - ipcMain.handle('nvidia-smi', async () => { - const ret = await common.nvidiaSmi(); - return ret; - }); ipcMain.handle('get-pipeline-list', async (_, settings: Settings) => { const ret = await common.getPipelineList(settings); return ret; @@ -29,21 +27,31 @@ export default function register() { }); /** - * TODO: replace linux defaults with some kind of platform switching logic + * Platform-dependent methods */ + // defaults to linux if win32 doesn't exist + const currentPlatform = OS.platform() === 'win32' ? win32 : linux; + if (OS.platform() === 'win32') { + win32.initialize(); + } + ipcMain.handle('nvidia-smi', async () => { + const ret = await currentPlatform.nvidiaSmi(); + return ret; + }); + ipcMain.handle('default-settings', async () => { - const defaults = linux.DefaultSettings; + const defaults = currentPlatform.DefaultSettings; return defaults; }); ipcMain.handle('validate-settings', async (_, settings: Settings) => { - const ret = await linux.validateViamePath(settings); + const ret = await currentPlatform.validateViamePath(settings); return ret; }); ipcMain.handle('run-pipeline', async (event, args: RunPipeline) => { const updater = (update: DesktopJobUpdate) => { event.sender.send('job-update', update); }; - return linux.runPipeline(args, updater); + return currentPlatform.runPipeline(args, updater); }); } diff --git a/client/platform/desktop/backend/platforms/common.ts b/client/platform/desktop/backend/platforms/common.ts index be843ac69..20b53c8e4 100644 --- a/client/platform/desktop/backend/platforms/common.ts +++ b/client/platform/desktop/backend/platforms/common.ts @@ -3,15 +3,13 @@ */ import npath from 'path'; import fs from 'fs-extra'; -import { spawn } from 'child_process'; import { shell } from 'electron'; import mime from 'mime-types'; -import { xml2json } from 'xml-js'; import moment from 'moment'; import { TrackData } from 'vue-media-annotator/track'; import { DatasetType, Pipelines, SaveDetectionsArgs } from 'viame-web-common/apispec'; -import { Settings, NvidiaSmiReply, websafeImageTypes } from '../../constants'; +import { Settings, websafeImageTypes } from '../../constants'; import * as viameSerializers from '../serializers/viame'; const AuxFolderName = 'auxiliary'; @@ -193,35 +191,6 @@ async function createKwiverRunWorkingDir(datasetName: string, baseDir: string, p return runFolderPath; } -// Based on https://github.com/chrisallenlane/node-nvidia-smi -async function nvidiaSmi(): Promise { - return new Promise((resolve) => { - const smi = spawn('nvidia-smi', ['-q', '-x']); - let result = ''; - smi.stdout.on('data', (chunk) => { - result = result.concat(chunk.toString('utf-8')); - }); - smi.on('close', (exitCode) => { - let jsonStr = 'null'; // parses to null - if (exitCode === 0) { - jsonStr = xml2json(result, { compact: true }); - } - resolve({ - output: JSON.parse(jsonStr), - exitCode, - error: result, - }); - }); - smi.on('error', (err) => { - resolve({ - output: null, - exitCode: -1, - error: err.message, - }); - }); - }); -} - /** * Save pre-serialized tracks to disk * @param datasetId path @@ -297,7 +266,6 @@ async function openLink(url: string) { } export default { - nvidiaSmi, openLink, getAuxFolder, createKwiverRunWorkingDir, diff --git a/client/platform/desktop/backend/platforms/linux.ts b/client/platform/desktop/backend/platforms/linux.ts index ede7af9e7..b10875883 100644 --- a/client/platform/desktop/backend/platforms/linux.ts +++ b/client/platform/desktop/backend/platforms/linux.ts @@ -4,10 +4,12 @@ import npath from 'path'; import { spawn } from 'child_process'; import fs from 'fs-extra'; +import { xml2json } from 'xml-js'; import { Settings, SettingsCurrentVersion, DesktopJob, DesktopJobUpdate, RunPipeline, + NvidiaSmiReply, } from '../../constants'; import common from './common'; @@ -161,9 +163,39 @@ async function runPipeline( return jobBase; } +// Based on https://github.com/chrisallenlane/node-nvidia-smi +async function nvidiaSmi(): Promise { + return new Promise((resolve) => { + const smi = spawn('nvidia-smi', ['-q', '-x']); + let result = ''; + smi.stdout.on('data', (chunk) => { + result = result.concat(chunk.toString('utf-8')); + }); + smi.on('close', (exitCode) => { + let jsonStr = 'null'; // parses to null + if (exitCode === 0) { + jsonStr = xml2json(result, { compact: true }); + } + resolve({ + output: JSON.parse(jsonStr), + exitCode, + error: result, + }); + }); + smi.on('error', (err) => { + resolve({ + output: null, + exitCode: -1, + error: err.message, + }); + }); + }); +} + export default { DefaultSettings, validateViamePath, runPipeline, + nvidiaSmi, }; diff --git a/client/platform/desktop/backend/platforms/windows.ts b/client/platform/desktop/backend/platforms/windows.ts index 772bbd42a..b4529c97a 100644 --- a/client/platform/desktop/backend/platforms/windows.ts +++ b/client/platform/desktop/backend/platforms/windows.ts @@ -1,3 +1,251 @@ /** * VIAME process manager for windows platform */ +import npath from 'path'; +import { spawn } from 'child_process'; +import { app } from 'electron'; +import fs from 'fs-extra'; +import { xml2json } from 'xml-js'; + +import { + Settings, SettingsCurrentVersion, + DesktopJob, DesktopJobUpdate, RunPipeline, + NvidiaSmiReply, +} from '../../constants'; + +import common from './common'; + +const DefaultSettings: Settings = { + // The current settings schema config + version: SettingsCurrentVersion, + // A path to the VIAME base install + viamePath: 'C:\\Program Files\\VIAME', + // Path to a user data folder + dataPath: app.getPath('home'), +}; + +let programFiles = 'C:\\Program Files'; +// There exists no app.getPath('programfiles') so we need to +// check the variable for the default location +async function initialize() { + const environmentVarPath = spawn('cmd.exe', ['/c', 'echo %PROGRAMFILES%'], { shell: true }); + environmentVarPath.stdout.on('data', (data) => { + const trimmed = data.toString().trim(); + programFiles = trimmed; + DefaultSettings.viamePath = `${trimmed}\\VIAME`; + }); +} + +async function validateViamePath(settings: Settings): Promise { + const setupScriptPath = npath.join(settings.viamePath, 'setup_viame.bat'); + const setupExists = await fs.pathExists(setupScriptPath); + if (!setupExists) { + return `${setupScriptPath} does not exist`; + } + + const modifiedCommand = `"${setupScriptPath.replace(/\\/g, '\\')}"`; + const kwiverExistsOnPath = spawn( + `${modifiedCommand} && kwiver.exe help`, { + shell: true, + }, + ); + return new Promise((resolve) => { + kwiverExistsOnPath.on('exit', (code) => { + if (code === 0) { + resolve(true); + } else { + resolve('kwiver failed to initialize'); + } + }); + }); +} + +/** + * Fashioned as a node.js implementation of viame_tasks.tasks.run_pipeline + * + * @param datasetIdPath dataset path absolute + * @param pipeline pipeline file basename + * @param settings global settings + */ +async function runPipeline( + runPipelineArgs: RunPipeline, + updater: (msg: DesktopJobUpdate) => void, +): Promise { + const { settings, datasetId, pipelineName } = runPipelineArgs; + const isValid = await validateViamePath(settings); + if (isValid !== true) { + throw new Error(isValid); + } + + const setupScriptPath = npath.join(settings.viamePath, 'setup_viame.bat'); + const pipelinePath = npath.join(settings.viamePath, 'configs/pipelines', pipelineName); + const datasetInfo = await common.getDatasetBase(datasetId); + const auxPath = await common.getAuxFolder(datasetInfo.basePath); + const jobWorkDir = await common.createKwiverRunWorkingDir( + datasetInfo.name, auxPath, pipelineName, + ); + + const detectorOutput = npath.join(jobWorkDir, 'detector_output.csv'); + const trackOutput = npath.join(jobWorkDir, 'track_output.csv'); + const joblog = npath.join(jobWorkDir, 'runlog.txt'); + + const modifiedCommand = `"${setupScriptPath.replace(/\\/g, '\\')}"`; + + let command: string[] = []; + if (datasetInfo.datasetType === 'video') { + command = [ + `${modifiedCommand} &&`, + 'kwiver.exe runner', + '-s input:video_reader:type=vidl_ffmpeg', + `-p ${pipelinePath}`, + `-s input:video_filename=${datasetId}`, + `-s detector_writer:file_name=${detectorOutput}`, + `-s track_writer:file_name=${trackOutput}`, + ]; + } else if (datasetInfo.datasetType === 'image-sequence') { + // Create frame image manifest + const manifestFile = npath.join(jobWorkDir, 'image-manifest.txt'); + // map image file names to absolute paths + const fileData = datasetInfo.imageFiles + .map((f) => npath.join(datasetInfo.basePath, f)) + .join('\n'); + await fs.writeFile(manifestFile, fileData); + command = [ + `${modifiedCommand} &&`, + 'kwiver.exe runner', + `-p "${pipelinePath}"`, + `-s input:video_filename="${manifestFile}"`, + `-s detector_writer:file_name="${detectorOutput}"`, + `-s track_writer:file_name="${trackOutput}"`, + ]; + } + + const job = spawn(command.join(' '), { + shell: true, + cwd: jobWorkDir, + }); + + const jobBase: DesktopJob = { + key: `pipeline_${job.pid}_${jobWorkDir}`, + jobType: 'pipeline', + pid: job.pid, + pipelineName, + workingDir: jobWorkDir, + datasetIds: [datasetId], + exitCode: job.exitCode, + startTime: new Date(), + }; + + const processChunk = (chunk: Buffer) => chunk + .toString('utf-8') + .split('\n') + .filter((a) => a); + + job.stdout.on('data', (chunk: Buffer) => { + // eslint-disable-next-line no-console + console.log(chunk.toString('utf-8')); + updater({ + ...jobBase, + body: processChunk(chunk), + }); + // No way in windows to display and log stdout at same time without 3rd party tools + fs.appendFile(joblog, chunk.toString('utf-8'), (err) => { + if (err) throw err; + }); + }); + + job.stderr.on('data', (chunk: Buffer) => { + // eslint-disable-next-line no-console + console.log(chunk.toString('utf-8')); + updater({ + ...jobBase, + body: processChunk(chunk), + }); + fs.appendFile(joblog, chunk.toString('utf-8'), (err) => { + if (err) throw err; + }); + }); + + job.on('exit', async (code) => { + if (code === 0) { + try { + await common.postprocess([trackOutput, detectorOutput], datasetId); + } catch (err) { + console.error(err); + } + } + updater({ + ...jobBase, + body: [''], + exitCode: code, + endTime: new Date(), + }); + }); + + return jobBase; +} + +function checkDefaultNvidiaSmi( + resolve: (value?: NvidiaSmiReply | PromiseLike | undefined) => void, +) { + const smi = spawn(`"${programFiles}\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe"`, ['-q', '-x'], { shell: true }); + let result = ''; + smi.stdout.on('data', (chunk) => { + result = result.concat(chunk.toString('utf-8')); + }); + + smi.on('close', (exitCode) => { + let jsonStr = 'null'; // parses to null + if (exitCode === 0) { + jsonStr = xml2json(result, { compact: true }); + } + resolve({ + output: JSON.parse(jsonStr), + exitCode, + error: result, + }); + }); + smi.on('error', (err) => { + resolve({ + output: null, + exitCode: -1, + error: err.message, + }); + }); +} +// Note: this is the most recent location for the nvidia-smi +// it doesn't guarantee that the system doesn't have a relevant GPU +async function nvidiaSmi(): Promise { + return new Promise((resolve) => { + const pathsmi = spawn('nvidia-smi', ['-q', '-x'], { shell: true }); + let result = ''; + pathsmi.stdout.on('data', (chunk) => { + console.log(chunk.toString('utf-8')); + result = result.concat(chunk.toString('utf-8')); + }); + + pathsmi.on('close', (exitCode) => { + let jsonStr = 'null'; // parses to null + if (exitCode === 0) { + jsonStr = xml2json(result, { compact: true }); + resolve({ + output: JSON.parse(jsonStr), + exitCode, + error: result, + }); + } else { + checkDefaultNvidiaSmi(resolve); + } + }); + pathsmi.on('error', () => { + checkDefaultNvidiaSmi(resolve); + }); + }); +} +export default { + DefaultSettings, + validateViamePath, + runPipeline, + nvidiaSmi, + initialize, +}; diff --git a/client/platform/desktop/components/Settings.vue b/client/platform/desktop/components/Settings.vue index b38800ff5..40bae633c 100644 --- a/client/platform/desktop/components/Settings.vue +++ b/client/platform/desktop/components/Settings.vue @@ -3,6 +3,7 @@ import { defineComponent, onBeforeMount, ref, } from '@vue/composition-api'; +import { remote } from 'electron'; import { NvidiaSmiReply } from '../constants'; import { settings, setSettings, validateSettings } from '../store/settings'; @@ -29,8 +30,19 @@ export default defineComponent({ smi.value = await nvidiaSmi(); }); + async function openPath() { + const result = await remote.dialog.showOpenDialog({ + properties: ['openDirectory'], + defaultPath: localSettings.value.viamePath, + }); + if (!result.canceled) { + [localSettings.value.viamePath] = result.filePaths; + } + } + async function save() { if (settings.value !== null) { + settingsAreValid.value = false; settingsAreValid.value = await validateSettings(localSettings.value); setSettings(localSettings.value); } @@ -45,6 +57,7 @@ export default defineComponent({ settingsAreValid, smi, version, + openPath, }; }, }); @@ -57,13 +70,32 @@ export default defineComponent({ Settings - + + + + + + + Open + + mdi-folder-open + + + + + - + + Checking GPU compatibility: + + + You are using a supported GPU configuration @@ -99,9 +137,17 @@ export default defineComponent({ dense text class="mx-4" - :type="settingsAreValid === true ? 'success' : 'warning'" + :type="settingsAreValid === + false ? 'info' : settingsAreValid === true ? 'success' : 'warning'" > - + + Checking for Kwiver + + + Kwiver initialization succeeded