Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show progress bar when flashing Tachyon #785

Merged
merged 6 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/qdl/darwin/arm64/qdl
Binary file not shown.
Binary file modified assets/qdl/darwin/x64/qdl
Binary file not shown.
Binary file modified assets/qdl/linux/x64/qdl
Binary file not shown.
Binary file modified assets/qdl/win32/x64/qdl.exe
Binary file not shown.
6 changes: 5 additions & 1 deletion src/cli/flash.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ module.exports = ({ commandProcessor, root }) => {
'tachyon' : {
boolean: true,
description: 'Flash Tachyon'
}
},
'output': {
describe: 'Folder to output the log file. Only available for Tachyon'
},
},
handler: (args) => {
const FlashCommand = require('../cmd/flash');
Expand All @@ -61,6 +64,7 @@ module.exports = ({ commandProcessor, root }) => {
'$0 $command --usb firmware.bin': 'Flash the binary over USB',
'$0 $command --tachyon': 'Flash Tachyon from the files in the current directory',
'$0 $command --tachyon /path/to/unpackaged-tool-and-files': 'Flash Tachyon from the files in the specified directory',
'$0 $command --tachyon /path/to/package.zip --output /path/to/log-folder': 'Flash Tachyon using the specified zip file and save the log to the given folder',
},
epilogue: unindent(`
When passing the --local flag, Device OS will be updated if the version on the device is outdated.
Expand Down
50 changes: 25 additions & 25 deletions src/cmd/flash.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const {
const createApiCache = require('../lib/api-cache');
const { validateDFUSupport } = require('./device-util');
const unzip = require('unzipper');
const qdl = require('../lib/qdl');
const QdlFlasher = require('../lib/qdl');

const TACHYON_MANIFEST_FILE = 'manifest.json';

Expand All @@ -44,8 +44,8 @@ module.exports = class FlashCommand extends CLICommandBase {
target,
port,
yes,
verbose,
tachyon,
output,
'application-only': applicationOnly
}) {
if (!tachyon && !device && !binary && !local) {
Expand All @@ -64,14 +64,14 @@ module.exports = class FlashCommand extends CLICommandBase {
await this.flashLocal({ files: allFiles, applicationOnly, target });
} else if (tachyon) {
let allFiles = binary ? [binary, ...files] : files;
await this.flashTachyon({ verbose, files: allFiles });
await this.flashTachyon({ files: allFiles, output });
} else {
await this.flashCloud({ device, files, target });
}
}

async flashTachyon({ verbose, files }) {
this.ui.write(`${os.EOL}Ensure only one device is connected to the computer${os.EOL}`);
async flashTachyon({ files, output }) {
this.ui.write(`${os.EOL}Ensure that only one device is connected to the computer before proceeding.${os.EOL}`);

let zipFile;
let includeDir = '';
Expand Down Expand Up @@ -101,27 +101,27 @@ module.exports = class FlashCommand extends CLICommandBase {
filesToProgram = files;
}

this.ui.write(`Starting download. The download may take several minutes...${os.EOL}`);

const res = await qdl.run({
files: filesToProgram,
includeDir,
updateFolder,
zip: zipFile,
verbose,
ui: this.ui
});
// put the output in a log file if not verbose
if (!verbose) {
const outputLog = path.join(process.cwd(), `qdl-output-${Date.now()}.log`);
if (res?.stdout) {
await fs.writeFile(outputLog, res.stdout);
}
this.ui.write(`Download complete. Output log available at ${outputLog}${os.EOL}`);
} else {
this.ui.write(`Download complete${os.EOL}`);
this.ui.write(`Starting download. This may take several minutes...${os.EOL}`);
if (output && !fs.existsSync(output)) {
fs.mkdirSync(output);
}
const outputLog = path.join(output ? output : process.cwd(), `tachyon_flash_${Date.now()}.log`);
try {
this.ui.write(`Logs are being written to: ${outputLog}${os.EOL}`);
const qdl = new QdlFlasher({
files: filesToProgram,
includeDir,
updateFolder,
zip: zipFile,
ui: this.ui,
outputLogFile: outputLog
});
await qdl.run();
fs.appendFileSync(outputLog, 'Download complete.');
} catch (error) {
this.ui.write('Download failed');
fs.appendFileSync(outputLog, 'Download failed with error: ' + error.message);
}
// TODO: Handle errors
}

async _extractFlashFilesFromDir(dirPath) {
Expand Down
175 changes: 143 additions & 32 deletions src/lib/qdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,157 @@ const mkdirTemp = util.promisify(temp.mkdir);

const TACHYON_STORAGE_TYPE = 'ufs';

async function getExecutable() {
const archType = utilities.getArchType();
const archName = utilities.getOs();
const qdlDir = path.join(__dirname, `../../assets/qdl/${archName}/${archType}`);
if (!await fs.pathExists(qdlDir)) {
throw new Error('Flashing Tachyon is not suppported on your OS');
class QdlFlasher {
constructor({ files, includeDir, updateFolder, zip, ui, outputLogFile }) {
this.files = files;
this.includeDir = includeDir;
this.updateFolder = updateFolder;
this.zip = zip;
this.ui = ui;
this.outputLogFile = outputLogFile;
this.progressBar = null;
this.totalSectorsInAllFiles = 0;
this.totalSectorsFlashed = 0;
this.currentModuleName = '';
this.currentModuleSectors = 0;
this.progressBarInitialized = false;
this.preparingDownload = false;
}

// Copy qdl to a temporary directory, so it can run outside the pkg snapshot
const tmpDir = await mkdirTemp('qdl');
await fs.copy(qdlDir, tmpDir);
async run() {
try {
const qdlPath = await this.getExecutable();
const qdlArguments = this.buildArgs({ files: this.files, includeDir: this.includeDir, zip: this.zip });
this.progressBar = this.ui.createProgressBar();
const command = `${qdlPath} ${qdlArguments.join(' ')}`;
fs.appendFileSync(this.outputLogFile, `Command: ${command}\n`);

return path.join(tmpDir, 'qdl' + (archName === 'win32' ? '.exe' : ''));
}
const qdlProcess = execa(qdlPath, qdlArguments, {
cwd: this.updateFolder || process.cwd(),
stdio: 'pipe'
});

const handleStream = (stream) => {
stream.on('data', chunk => {
chunk.toString().split('\n').map(line => line.trim()).filter(Boolean).forEach(line => {
this.processLogLine(line, qdlProcess);
});
});
};

handleStream(qdlProcess.stdout);
handleStream(qdlProcess.stderr);

await qdlProcess;
} finally {
if (this.progressBarInitialized) {
this.progressBar.stop();
}
}
}

async getExecutable() {
const archType = utilities.getArchType();
const archName = utilities.getOs();
const qdlDir = path.join(__dirname, `../../assets/qdl/${archName}/${archType}`);
if (!await fs.pathExists(qdlDir)) {
throw new Error('Flashing Tachyon is not suppported on your OS');
}

/**
*/
async function run({ files, includeDir, updateFolder, zip, verbose, ui }) {
const qdl = await getExecutable();
// Copy qdl to a temporary directory, so it can run outside the pkg snapshot
const tmpDir = await mkdirTemp('qdl');
await fs.copy(qdlDir, tmpDir);

const qdlArgs = [
'--storage',
TACHYON_STORAGE_TYPE,
...(zip ? ['--zip', zip] : []),
...(includeDir ? ['--include', includeDir] : []),
...files
];
return path.join(tmpDir, 'qdl' + (archName === 'win32' ? '.exe' : ''));
}

buildArgs({ files, includeDir, zip }) {
return [
'--storage', TACHYON_STORAGE_TYPE,
...(zip ? ['--zip', zip] : []),
...(includeDir ? ['--include', includeDir] : []),
...files
];
}

if (verbose) {
ui.write(`Command: ${qdl} ${qdlArgs.join(' ')}${os.EOL}`);
processLogLine(line, process) {
fs.appendFileSync(this.outputLogFile, `${line}\n`);

if (line.includes('Waiting for EDL device')) {
this.handleError(process, `Ensure your device is connected and in EDL mode${os.EOL}`);
} else if (line.includes('[ERROR]')) {
this.handleError(process, `${os.EOL}Error detected: ${line}${os.EOL}`);
} else {
this.processFlashingLogs(line);
}
}

const res = await execa(qdl, qdlArgs, {
cwd: updateFolder || process.cwd(),
stdio: verbose ? 'inherit' : 'pipe'
});
handleError(process, message) {
this.ui.stdout.write(message);
process.kill();
}

processFlashingLogs(line) {
if (line.includes('status=getProgramInfo')) {
this.handleProgramInfo(line);
} else if (line.includes('status=Start flashing module')) {
this.handleModuleStart(line);
} else if (line.includes('status=Flashing module')) {
this.handleModuleProgress(line);
}
}

handleProgramInfo(line) {
if (!this.preparingDownload) {
this.preparingDownload = true;
this.ui.stdout.write('Preparing to download files...');
}
const match = line.match(/sectors_total=(\d+)/);
if (match) {
this.totalSectorsInAllFiles += parseInt(match[1], 10);
}
}

handleModuleStart(line) {
const moduleNameMatch = line.match(/module=(.*?),/);
const sectorsTotalMatch = line.match(/sectors_total=(\d+)/);
if (moduleNameMatch && sectorsTotalMatch) {
this.currentModuleName = moduleNameMatch[1];
this.currentModuleSectors = parseInt(sectorsTotalMatch[1], 10);

if (!this.progressBarInitialized) {
this.progressBarInitialized = true;
this.progressBar.start(this.totalSectorsInAllFiles, this.totalSectorsFlashed, {
description: `Flashing ${this.currentModuleName}`
});
} else {
this.progressBar.update(this.totalSectorsFlashed, {
description: `Flashing ${this.currentModuleName}`
});
}
}
}

handleModuleProgress(line) {
const sectorsFlashedMatch = line.match(/sectors_done=(\d+)/);
if (sectorsFlashedMatch) {
const sectorsFlashed = parseInt(sectorsFlashedMatch[1], 10);
this.progressBar.update(this.totalSectorsFlashed + sectorsFlashed, {
description: `Flashing module: ${this.currentModuleName} (${sectorsFlashed}/${this.currentModuleSectors} sectors)`
});

if (sectorsFlashed === this.currentModuleSectors) {
this.totalSectorsFlashed += this.currentModuleSectors;
this.progressBar.update({ description: `Flashed ${this.currentModuleName}` });
}

if (this.totalSectorsFlashed === this.totalSectorsInAllFiles) {
this.progressBar.update({ description: 'Flashing complete' });
}
}
}

return res;
}

module.exports = {
run
};

module.exports = QdlFlasher;
Loading