Update transfers.service.ts

This commit is contained in:
Baobhan Sith
2025-05-16 19:24:57 +08:00
parent a6dac55045
commit 1dcdea2425
@@ -164,24 +164,92 @@ export class TransfersService {
} }
} }
private async checkCommandOnSource(client: Client, command: string): Promise<boolean> { private async checkCommandOnSource(client: Client, command: string): Promise<string | null> {
return new Promise((resolve) => { return new Promise((resolve) => {
client.exec(`command -v ${command}`, (err, stream) => { const checkCmd = `command -v ${this.escapeShellArg(command)} 2>/dev/null`;
console.error(`[Roo Debug][transfers.service.ts] checkCommandOnSource: Executing: ${checkCmd}`);
client.exec(checkCmd, (err, stream) => {
if (err) { if (err) {
console.warn(`[TransfersService] Error checking for command '${command}' on source:`, err); console.warn(`[Roo Debug][transfers.service.ts] Error checking for command '${command}' on source:`, err);
return resolve(false); return resolve(null);
} }
let stdout = ''; let stdout = '';
stream stream
.on('data', (data: Buffer) => stdout += data.toString()) .on('data', (data: Buffer) => stdout += data.toString())
.on('close', (code: number) => { .on('close', (code: number) => {
resolve(code === 0 && stdout.trim() !== ''); const foundPath = stdout.trim();
if (code === 0 && foundPath) {
console.error(`[Roo Debug][transfers.service.ts] checkCommandOnSource: Command '${command}' found at '${foundPath}'.`);
resolve(foundPath);
} else {
console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnSource: Command '${command}' not found (exit code: ${code}).`);
resolve(null);
}
})
.stderr.on('data', (data: Buffer) => { // Should be empty due to 2>/dev/null, but good to have
console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnSource: STDERR for '${command}': ${data.toString()}`);
});
});
});
}
private async checkCommandOnTargetServer(targetConnection: ConnectionWithTags, targetCredentials: DecryptedConnectionCredentials, command: string): Promise<string | null> {
const targetClient = new Client();
const connectConfig = this.buildSshConnectConfig(targetConnection, targetCredentials);
let foundCommandPath: string | null = null;
console.error(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Attempting to connect to target ${targetConnection.host} to check for command '${command}'.`);
try {
await new Promise<void>((resolve, reject) => {
targetClient
.on('ready', () => {
console.info(`[TransfersService] SSH connection established to target server ${targetConnection.host} for command check.`);
resolve();
})
.on('error', (err) => {
console.error(`[TransfersService] SSH connection error to target server ${targetConnection.host} for command check:`, err);
reject(err);
})
.on('close', () => {
console.info(`[TransfersService] SSH connection closed to target server ${targetConnection.host} after command check.`);
})
.connect(connectConfig);
});
foundCommandPath = await new Promise((resolve) => {
const checkCmd = `command -v ${this.escapeShellArg(command)} 2>/dev/null`;
console.error(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Executing on target: ${checkCmd}`);
targetClient.exec(checkCmd, (err, stream) => {
if (err) {
console.warn(`[Roo Debug][transfers.service.ts] Error checking for command '${command}' on target ${targetConnection.host}:`, err);
return resolve(null);
}
let stdout = '';
stream
.on('data', (data: Buffer) => stdout += data.toString())
.on('close', (code: number) => {
const pathOutput = stdout.trim();
if (code === 0 && pathOutput) {
console.error(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Command '${command}' found at '${pathOutput}' on target ${targetConnection.host}.`);
resolve(pathOutput);
} else {
console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Command '${command}' not found on target ${targetConnection.host} (exit code: ${code}).`);
resolve(null);
}
}) })
.stderr.on('data', (data: Buffer) => { .stderr.on('data', (data: Buffer) => {
console.warn(`[TransfersService] STDERR checking for command '${command}' on source: ${data.toString()}`); console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: STDERR for '${command}' on target ${targetConnection.host}: ${data.toString()}`);
}); });
}); });
}); });
} catch (error) {
console.error(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Failed to check command '${command}' on target ${targetConnection.host}:`, error);
foundCommandPath = null; // Ensure it's null on error
} finally {
targetClient.end();
}
return foundCommandPath;
} }
private async uploadKeyToSourceViaSftp(client: Client, privateKeyContent: string, remotePath: string): Promise<void> { private async uploadKeyToSourceViaSftp(client: Client, privateKeyContent: string, remotePath: string): Promise<void> {
@@ -271,7 +339,8 @@ export class TransfersService {
isDir: boolean, isDir: boolean,
targetConnection: ConnectionWithTags, // Target B connection details targetConnection: ConnectionWithTags, // Target B connection details
targetPathOnB: string, // Base remote target path on B targetPathOnB: string, // Base remote target path on B
transferCmd: 'scp' | 'rsync', executableCommand: string, // Full path to rsync or scp
commandType: 'rsync' | 'scp', // To distinguish logic
options: { // Options derived from checking source A and target B auth options: { // Options derived from checking source A and target B auth
sshPassCommand?: string; // e.g., "sshpass -p 'password'" sshPassCommand?: string; // e.g., "sshpass -p 'password'"
sshIdentityFileOption?: string; // e.g., "-i /tmp/key_B_XYZ" sshIdentityFileOption?: string; // e.g., "-i /tmp/key_B_XYZ"
@@ -280,38 +349,50 @@ export class TransfersService {
} }
): string { ): string {
const remoteBase = targetPathOnB.endsWith('/') ? targetPathOnB : `${targetPathOnB}/`; const remoteBase = targetPathOnB.endsWith('/') ? targetPathOnB : `${targetPathOnB}/`;
const remoteFullDest = `${options.targetUserAndHost}:${this.escapeShellArg(remoteBase)}`; // SCP/Rsync will append filename if source is file const remoteFullDest = `${options.targetUserAndHost}:${this.escapeShellArg(remoteBase)}`;
let commandParts: string[] = []; let commandParts: string[] = [];
if (options.sshPassCommand) { if (options.sshPassCommand) {
commandParts.push(options.sshPassCommand); commandParts.push(options.sshPassCommand);
} }
if (transferCmd === 'rsync') { // Use the full path here (should be safe, no special chars from command -v)
commandParts.push('rsync -avz --progress'); // Arguments will still be quoted later.
commandParts.push(executableCommand);
if (commandType === 'rsync') {
commandParts.push('-avz --progress'); // rsync specific options
// For rsync, SSH options go into the -e argument
let sshArgsForRsync = `ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`; let sshArgsForRsync = `ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`;
if (options.sshPortOption && options.sshPortOption.startsWith('-p')) { // for rsync -e "ssh -p XXX" if (options.sshPortOption && options.sshPortOption.startsWith('-p')) { // rsync uses -p for port in its -e "ssh -p XXX"
sshArgsForRsync += ` ${options.sshPortOption}`; sshArgsForRsync += ` ${options.sshPortOption}`;
} }
if (options.sshIdentityFileOption) { if (options.sshIdentityFileOption) { // -i for identity file is an ssh option
sshArgsForRsync += ` ${options.sshIdentityFileOption}`; sshArgsForRsync += ` ${options.sshIdentityFileOption}`;
} }
commandParts.push(`-e "${sshArgsForRsync.trim()}"`); commandParts.push(`-e "${sshArgsForRsync.trim()}"`);
let rsyncSourcePath = this.escapeShellArg(sourceItemPathOnA); let rsyncSourcePath = this.escapeShellArg(sourceItemPathOnA);
if (isDir && !rsyncSourcePath.endsWith('/\'')) { // if escaped and ends with /' if (isDir && !rsyncSourcePath.endsWith('/\'')) {
rsyncSourcePath = rsyncSourcePath.slice(0, -1) + '/\''; // Add trailing slash for rsync dir content copy rsyncSourcePath = rsyncSourcePath.slice(0, -1) + '/\'';
} }
commandParts.push(rsyncSourcePath); commandParts.push(rsyncSourcePath);
commandParts.push(remoteFullDest); commandParts.push(remoteFullDest);
} else { // scp } else { // scp
commandParts.push('scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'); // For scp, SSH options are typically passed directly if scp is a wrapper around ssh, or via scp's own options that map to ssh options.
// Common scp implementations accept -P for port and -i for identity file directly.
// StrictHostKeyChecking and UserKnownHostsFile are ssh options.
// We build the ssh part for scp separately if needed, or rely on scp passing -o options.
// Let's assume scp will pass these -o options to its underlying ssh call.
// If not, a more complex construction of scp's ssh command via -S might be needed.
commandParts.push('-o StrictHostKeyChecking=no'); // For scp, pass as direct option
commandParts.push('-o UserKnownHostsFile=/dev/null'); // For scp, pass as direct option
if (isDir) commandParts.push('-r'); if (isDir) commandParts.push('-r');
if (options.sshPortOption && options.sshPortOption.startsWith('-P')) { // for scp -P XXX if (options.sshPortOption && options.sshPortOption.startsWith('-P')) { // scp uses -P for port
commandParts.push(options.sshPortOption); commandParts.push(options.sshPortOption);
} }
if (options.sshIdentityFileOption) { if (options.sshIdentityFileOption) { // scp uses -i for identity file
commandParts.push(options.sshIdentityFileOption); commandParts.push(options.sshIdentityFileOption);
} }
commandParts.push(this.escapeShellArg(sourceItemPathOnA)); commandParts.push(this.escapeShellArg(sourceItemPathOnA));
@@ -333,30 +414,97 @@ export class TransfersService {
): Promise<void> { ): Promise<void> {
console.error(`[Roo Debug][transfers.service.ts] ENTERING executeRemoteTransferOnSource for sub-task ${subTaskId}, item: ${sourceItem.name}`); console.error(`[Roo Debug][transfers.service.ts] ENTERING executeRemoteTransferOnSource for sub-task ${subTaskId}, item: ${sourceItem.name}`);
this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 0, `Initializing remote transfer for ${sourceItem.name}`); this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 0, `Initializing remote transfer for ${sourceItem.name}`);
let tempTargetKeyPathOnSource: string | undefined; // Path of target's private key if temporarily on source A let tempTargetKeyPathOnSource: string | undefined;
try { try {
console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Starting try block in executeRemoteTransferOnSource.`); console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Starting try block in executeRemoteTransferOnSource.`);
const sshpassAvailableOnSource = await this.checkCommandOnSource(sourceSshClient, 'sshpass'); const sshpassPath = await this.checkCommandOnSource(sourceSshClient, 'sshpass');
const rsyncAvailableOnSource = await this.checkCommandOnSource(sourceSshClient, 'rsync'); const rsyncPathOnSource = await this.checkCommandOnSource(sourceSshClient, 'rsync'); // Renamed for clarity
const scpPathOnSource = await this.checkCommandOnSource(sourceSshClient, 'scp'); // Renamed for clarity
let determinedTransferCmd: 'scp' | 'rsync' = 'scp'; // Default to scp console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Source checks -> sshpass: ${sshpassPath}, rsync: ${rsyncPathOnSource}, scp: ${scpPathOnSource}`);
if (transferMethodPreference === 'rsync' && rsyncAvailableOnSource) {
determinedTransferCmd = 'rsync'; let executableCommandPath: string | null = null;
} else if (transferMethodPreference === 'rsync' && !rsyncAvailableOnSource) { let commandTypeForLogic: 'rsync' | 'scp' | undefined = undefined; // Initialize as undefined
this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, `Rsync preferred but not available on source server. Sub-task for ${sourceItem.name} failed.`); let rsyncPathOnTarget: string | null = null;
throw new Error('Rsync preferred but not available on source server.');
} else if (transferMethodPreference === 'auto') { if (transferMethodPreference === 'auto') {
determinedTransferCmd = rsyncAvailableOnSource ? 'rsync' : 'scp'; if (rsyncPathOnSource) {
// Source has rsync, check target
console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: 'auto' mode, rsync found on source. Checking target...`);
rsyncPathOnTarget = await this.checkCommandOnTargetServer(targetConnection, targetCredentials, 'rsync');
if (rsyncPathOnTarget) {
console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Target check (for auto rsync) -> rsync found at '${rsyncPathOnTarget}'. Selecting rsync.`);
executableCommandPath = rsyncPathOnSource; // Use source path for exec
commandTypeForLogic = 'rsync';
} else {
console.warn(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Rsync found on source, but NOT on target. Rsync cannot be used for 'auto' mode.`);
}
}
// If rsync not chosen (either source or target missing), try SCP for 'auto'
if (!commandTypeForLogic) {
if (scpPathOnSource) {
executableCommandPath = scpPathOnSource;
commandTypeForLogic = 'scp';
console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: 'auto' mode falling back to SCP (Source SCP path: ${scpPathOnSource}).`);
} else {
const msg = `Neither Rsync (source/target) nor SCP (source) are available for ${sourceItem.name} (auto mode).`;
this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, msg);
throw new Error(msg);
}
}
} else if (transferMethodPreference === 'rsync') {
if (rsyncPathOnSource) {
console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: 'rsync' preference, rsync found on source. Checking target...`);
rsyncPathOnTarget = await this.checkCommandOnTargetServer(targetConnection, targetCredentials, 'rsync');
if (rsyncPathOnTarget) {
console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Target check (for preferred rsync) -> rsync found at '${rsyncPathOnTarget}'. Selecting rsync.`);
executableCommandPath = rsyncPathOnSource;
commandTypeForLogic = 'rsync';
} else {
const msg = `Rsync preferred, found on source, but rsync is NOT available on target server for ${sourceItem.name}.`;
this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, msg);
throw new Error(msg);
}
} else {
const msg = `Rsync preferred but not available on source server for ${sourceItem.name}.`;
this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, msg);
throw new Error(msg);
}
} else if (transferMethodPreference === 'scp') {
if (scpPathOnSource) {
executableCommandPath = scpPathOnSource;
commandTypeForLogic = 'scp';
console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: 'scp' preference. Selecting SCP (Source SCP path: ${scpPathOnSource}).`);
} else {
const msg = `SCP preferred but not available on source server for ${sourceItem.name}.`;
this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, msg);
throw new Error(msg);
}
} else {
// This case should ideally not be reached if transferMethodPreference is correctly typed
const msg = `Invalid transfer method preference: ${transferMethodPreference}`;
this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, msg);
throw new Error(msg);
} }
this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 5, `Using ${determinedTransferCmd}. Source SSHPass: ${sshpassAvailableOnSource}, Rsync: ${rsyncAvailableOnSource}`);
const subTaskToUpdate = this.transferTasks.get(taskId)?.subTasks.find(st => st.subTaskId === subTaskId);
if (subTaskToUpdate) subTaskToUpdate.transferMethodUsed = determinedTransferCmd;
// Safeguard: ensure a command was actually selected
if (!executableCommandPath || !commandTypeForLogic) {
const msg = `Internal error: Could not determine a valid transfer command for ${sourceItem.name}. This should not happen.`;
console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: ${msg} rsyncSrc=${rsyncPathOnSource}, scpSrc=${scpPathOnSource}, rsyncTgt=${rsyncPathOnTarget}, pref=${transferMethodPreference}`);
this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, msg);
throw new Error(msg);
}
this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 5, `Using ${commandTypeForLogic} (Path: ${executableCommandPath}). Source SSHPass: ${!!sshpassPath}, Rsync Src: ${!!rsyncPathOnSource}, Rsync Tgt: ${!!rsyncPathOnTarget}, SCP Src: ${!!scpPathOnSource}`);
const subTaskToUpdate = this.transferTasks.get(taskId)?.subTasks.find(st => st.subTaskId === subTaskId);
if (subTaskToUpdate) subTaskToUpdate.transferMethodUsed = commandTypeForLogic;
const cmdOptions: any = { const cmdOptions: any = {
targetUserAndHost: `${targetConnection.username}@${targetConnection.host}`, targetUserAndHost: `${targetConnection.username}@${targetConnection.host}`,
sshPortOption: targetConnection.port ? (determinedTransferCmd === 'scp' ? `-P ${targetConnection.port}`: `-p ${targetConnection.port}`) : undefined, // Port for rsync is handled in buildTransferCommandString via -e "ssh -p <port>"
// Port for scp is -P <port>
sshPortOption: targetConnection.port ? (commandTypeForLogic === 'scp' ? `-P ${targetConnection.port}` : (commandTypeForLogic === 'rsync' ? `-p ${targetConnection.port}` : undefined)) : undefined,
}; };
if (targetConnection.auth_method === 'key' && targetCredentials.decryptedPrivateKey) { if (targetConnection.auth_method === 'key' && targetCredentials.decryptedPrivateKey) {
@@ -369,8 +517,8 @@ export class TransfersService {
cmdOptions.sshIdentityFileOption = `-i ${this.escapeShellArg(tempTargetKeyPathOnSource)}`; cmdOptions.sshIdentityFileOption = `-i ${this.escapeShellArg(tempTargetKeyPathOnSource)}`;
if (targetCredentials.decryptedPassphrase) { if (targetCredentials.decryptedPassphrase) {
if (sshpassAvailableOnSource) { if (sshpassPath) { // Check if sshpassPath is not null
cmdOptions.sshPassCommand = `sshpass -p ${this.escapeShellArg(targetCredentials.decryptedPassphrase)}`; cmdOptions.sshPassCommand = `${this.escapeShellArg(sshpassPath)} -p ${this.escapeShellArg(targetCredentials.decryptedPassphrase)}`;
} else { } else {
const msg = `Target key has passphrase, but sshpass is not available on source server for ${sourceItem.name}.`; const msg = `Target key has passphrase, but sshpass is not available on source server for ${sourceItem.name}.`;
this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, msg); this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, msg);
@@ -378,8 +526,8 @@ export class TransfersService {
} }
} }
} else if (targetConnection.auth_method === 'password' && targetCredentials.decryptedPassword) { } else if (targetConnection.auth_method === 'password' && targetCredentials.decryptedPassword) {
if (sshpassAvailableOnSource) { if (sshpassPath) { // Check if sshpassPath is not null
cmdOptions.sshPassCommand = `sshpass -p ${this.escapeShellArg(targetCredentials.decryptedPassword)}`; cmdOptions.sshPassCommand = `${this.escapeShellArg(sshpassPath)} -p ${this.escapeShellArg(targetCredentials.decryptedPassword)}`;
} else { } else {
const msg = `Target uses password auth, but sshpass is not available on source server for ${sourceItem.name}.`; const msg = `Target uses password auth, but sshpass is not available on source server for ${sourceItem.name}.`;
this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, msg); this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, msg);
@@ -397,24 +545,25 @@ export class TransfersService {
sourceItem.type === 'directory', sourceItem.type === 'directory',
targetConnection, targetConnection,
remoteTargetPathOnTarget, remoteTargetPathOnTarget,
determinedTransferCmd, executableCommandPath!, // Assert not null as we'd have thrown earlier
commandTypeForLogic,
cmdOptions cmdOptions
); );
console.info(`[TransfersService] Executing on source for sub-task ${subTaskId} (item: ${sourceItem.name}): ${commandToExecute}`); console.info(`[TransfersService] Executing on source for sub-task ${subTaskId} (item: ${sourceItem.name}): ${commandToExecute}`);
console.info(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Prepared command: ${commandToExecute}`); console.info(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Prepared command: ${commandToExecute}`);
console.info(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Command options: ${JSON.stringify(cmdOptions)}`); console.info(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Command options: ${JSON.stringify(cmdOptions)}`);
this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 10, `Executing: ${determinedTransferCmd} from source to ${targetConnection.name}`); this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 10, `Executing: ${commandTypeForLogic} from source to ${targetConnection.name}`);
const COMMAND_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes timeout for command execution const COMMAND_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes timeout for command execution
await new Promise<void>((resolveCmd, rejectCmd) => { await new Promise<void>((resolveCmd, rejectCmd) => {
let commandTimedOut = false; let commandTimedOut = false;
let stdoutCombined = ''; // Moved here to be accessible by timeout let stdoutCombined = '';
let stderrCombined = ''; // Moved here to be accessible by timeout let stderrCombined = '';
const timeoutHandle = setTimeout(() => { const timeoutHandle = setTimeout(() => {
commandTimedOut = true; commandTimedOut = true;
const timeoutMsg = `${determinedTransferCmd} command for ${sourceItem.name} timed out after ${COMMAND_TIMEOUT_MS / 1000}s.`; const timeoutMsg = `${commandTypeForLogic} command for ${sourceItem.name} timed out after ${COMMAND_TIMEOUT_MS / 1000}s.`;
console.warn(`[Roo Debug][transfers.service.ts] TIMEOUT ${timeoutMsg} (Sub-task: ${subTaskId})`); console.warn(`[Roo Debug][transfers.service.ts] TIMEOUT ${timeoutMsg} (Sub-task: ${subTaskId})`);
console.warn(`[Roo Debug][transfers.service.ts] TIMEOUT Sub-task ${subTaskId}: STDOUT at timeout: ${stdoutCombined}`); console.warn(`[Roo Debug][transfers.service.ts] TIMEOUT Sub-task ${subTaskId}: STDOUT at timeout: ${stdoutCombined}`);
console.warn(`[Roo Debug][transfers.service.ts] TIMEOUT Sub-task ${subTaskId}: STDERR at timeout: ${stderrCombined}`); console.warn(`[Roo Debug][transfers.service.ts] TIMEOUT Sub-task ${subTaskId}: STDERR at timeout: ${stderrCombined}`);
@@ -449,12 +598,12 @@ export class TransfersService {
stdoutCombined += output; stdoutCombined += output;
// More verbose logging for stdout // More verbose logging for stdout
console.debug(`[Roo Debug][transfers.service.ts] RAW STDOUT Sub-task ${subTaskId} (item ${sourceItem.name}): <<<${output}>>>`); console.debug(`[Roo Debug][transfers.service.ts] RAW STDOUT Sub-task ${subTaskId} (item ${sourceItem.name}): <<<${output}>>>`);
if (determinedTransferCmd === 'rsync') { if (commandTypeForLogic === 'rsync') {
const progressMatch = output.match(/(\d+)%/); const progressMatch = output.match(/(\d+)%/);
if (progressMatch && progressMatch[1]) { if (progressMatch && progressMatch[1]) {
this.updateSubTaskStatus(taskId, subTaskId, 'transferring', parseInt(progressMatch[1], 10)); this.updateSubTaskStatus(taskId, subTaskId, 'transferring', parseInt(progressMatch[1], 10));
} }
} else { } else { // scp
this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 50, 'SCP in progress...'); this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 50, 'SCP in progress...');
} }
}); });
@@ -478,23 +627,23 @@ export class TransfersService {
console.info(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Final STDERR: ${stderrCombined}`); console.info(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Final STDERR: ${stderrCombined}`);
if (code === 0) { if (code === 0) {
this.updateSubTaskStatus(taskId, subTaskId, 'completed', 100, `${determinedTransferCmd} successful for ${sourceItem.name} to ${targetConnection.name}.`); this.updateSubTaskStatus(taskId, subTaskId, 'completed', 100, `${commandTypeForLogic} successful for ${sourceItem.name} to ${targetConnection.name}.`);
resolveCmd(); resolveCmd();
} else { } else {
const errorMsg = `${determinedTransferCmd} failed for ${sourceItem.name} to ${targetConnection.name}. Exit code: ${code}, signal: ${signal}. Stderr: ${stderrCombined.trim()}`; const errorMsg = `${commandTypeForLogic} failed for ${sourceItem.name} to ${targetConnection.name}. Exit code: ${code}, signal: ${signal}. Stderr: ${stderrCombined.trim()}`;
this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, errorMsg); this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, errorMsg);
rejectCmd(new Error(errorMsg)); rejectCmd(new Error(errorMsg));
} }
}); });
stream.on('error', (streamErr: Error) => { // Should not happen if exec cb err is null stream.on('error', (streamErr: Error) => {
clearTimeout(timeoutHandle); clearTimeout(timeoutHandle);
if (commandTimedOut) { if (commandTimedOut) {
console.info(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Stream error AFTER timeout.`); console.info(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Stream error AFTER timeout.`);
return; return;
} }
console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Stream error event:`, streamErr); console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Stream error event:`, streamErr);
const errorMsg = `Stream error during ${determinedTransferCmd} for ${sourceItem.name}: ${streamErr.message}`; const errorMsg = `Stream error during ${commandTypeForLogic} for ${sourceItem.name}: ${streamErr.message}`;
this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, errorMsg); this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, errorMsg);
rejectCmd(streamErr); rejectCmd(streamErr);
}); });