Skip to content
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
38 changes: 30 additions & 8 deletions components/ts_automation/src/ts_action_manager.c
Original file line number Diff line number Diff line change
Expand Up @@ -1233,8 +1233,8 @@ esp_err_t ts_action_exec_ssh_ref(const ts_auto_action_ssh_ref_t *ssh_ref,
}
if (cmd_config.nohup) {
/* Generate safe name from command name for log/pid files
* Use cmd.name (user-readable identifier) for consistency
* varName is only for service mode status variables
* Priority: cmd.name alphanumeric chars -> cmd_id alphanumeric chars -> "cmd"
* Pure Chinese names produce empty string, so fallback to cmd_id
*/
char safe_name[32] = {0};
const char *src = cmd_config.name;
Expand All @@ -1246,18 +1246,30 @@ esp_err_t ts_action_exec_ssh_ref(const ts_auto_action_ssh_ref_t *ssh_ref,
safe_name[j++] = src[i];
}
}
if (j == 0) {
/* Fallback to cmd_id (e.g. "emb-pull-up" -> "embpullup") */
const char *id_src = ssh_ref->cmd_id;
for (int i = 0; id_src[i] && j < 20; i++) {
if ((id_src[i] >= 'a' && id_src[i] <= 'z') ||
(id_src[i] >= 'A' && id_src[i] <= 'Z') ||
(id_src[i] >= '0' && id_src[i] <= '9')) {
safe_name[j++] = id_src[i];
}
}
}
if (j == 0) {
strcpy(safe_name, "cmd");
}
ESP_LOGI(TAG, "nohup safe_name='%s' (from name='%s')", safe_name, cmd_config.name);
ESP_LOGI(TAG, "nohup safe_name='%s' (from name='%s', id='%s')", safe_name, cmd_config.name, ssh_ref->cmd_id);

/* nohup command with PID file for process tracking
* Format: nohup <cmd> > <log> 2>&1 & echo $! > <pid>
* The echo $! captures the background process PID
* Format: nohup <cmd> > <log> 2>&1 & echo $! > <pid>; sleep 0.3; cat <pid>
* 注意:$! 可能获取中间 shell PID 而非真实进程 PID(差 1),
* 前端已通过 PID-1 fallback 处理此情况
*/
snprintf(nohup_cmd, TS_SSH_CMD_COMMAND_MAX + 128,
"nohup %s > /tmp/ts_nohup_%s.log 2>&1 & echo $! > /tmp/ts_nohup_%s.pid",
expanded_cmd, safe_name, safe_name);
"nohup %s > /tmp/ts_nohup_%s.log 2>&1 & echo $! > /tmp/ts_nohup_%s.pid; sleep 0.3; cat /tmp/ts_nohup_%s.pid",
expanded_cmd, safe_name, safe_name, safe_name);
ESP_LOGI(TAG, "SSH nohup mode: %s", nohup_cmd);
}

Expand Down Expand Up @@ -1382,7 +1394,7 @@ esp_err_t ts_action_exec_ssh_ref(const ts_auto_action_ssh_ref_t *ssh_ref,
cmd_config.ready_pattern[0] && cmd_config.var_name[0]) {

/* Generate safe name (same logic as nohup wrapper above)
* Use cmd.name for file paths, var_name only for status variables
* Priority: cmd.name -> cmd_id -> "cmd"
*/
char safe_name[32] = {0};
const char *src = cmd_config.name;
Expand All @@ -1394,6 +1406,16 @@ esp_err_t ts_action_exec_ssh_ref(const ts_auto_action_ssh_ref_t *ssh_ref,
safe_name[j++] = src[i];
}
}
if (j == 0) {
const char *id_src = ssh_ref->cmd_id;
for (int i = 0; id_src[i] && j < 20; i++) {
if ((id_src[i] >= 'a' && id_src[i] <= 'z') ||
(id_src[i] >= 'A' && id_src[i] <= 'Z') ||
(id_src[i] >= '0' && id_src[i] <= '9')) {
safe_name[j++] = id_src[i];
}
}
}
if (j == 0) {
strcpy(safe_name, "cmd");
}
Expand Down
48 changes: 38 additions & 10 deletions components/ts_security/src/ts_ssh_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -620,15 +620,21 @@ esp_err_t ts_ssh_exec(ts_ssh_session_t session, const char *command, ts_ssh_exec
result->stdout_len = 0;
result->stderr_len = 0;

/* 循环读取直到完成 */
/* 循环读取直到 channel EOF。
* 内层 do-while 批量 drain 当前所有可用数据(rc > 0 时持续读)。
* 外层用 libssh2_channel_eof() 判断是否结束。
* 原来的 bug:drain 后 rc==0,走 else break 过早退出,丢失后续数据。
* 安全措施:非 EAGAIN 错误立即返回,防止无限循环;最多等 300 轮(约 5 分钟)。 */
int wait_rounds = 0;
for (;;) {
char buffer[512];

/* 读取 stdout */
int got_data = 0;

/* Drain stdout —— 一次性读完当前所有可用数据 */
do {
rc = libssh2_channel_read(channel, buffer, sizeof(buffer));
if (rc > 0) {
/* 扩展缓冲区 */
got_data = 1;
while (result->stdout_len + rc >= stdout_capacity) {
stdout_capacity *= 2;
char *new_buf = TS_REALLOC_PSRAM(result->stdout_data, stdout_capacity);
Expand All @@ -642,14 +648,19 @@ esp_err_t ts_ssh_exec(ts_ssh_session_t session, const char *command, ts_ssh_exec
}
memcpy(result->stdout_data + result->stdout_len, buffer, rc);
result->stdout_len += rc;
} else if (rc < 0 && rc != LIBSSH2_ERROR_EAGAIN) {
set_error(session, "Channel read error: %d", rc);
ts_ssh_exec_result_free(result);
libssh2_channel_free(channel);
return ESP_FAIL;
}
} while (rc > 0);

/* 读取 stderr */
/* Drain stderr —— 同理 */
do {
rc = libssh2_channel_read_stderr(channel, buffer, sizeof(buffer));
if (rc > 0) {
/* 扩展缓冲区 */
got_data = 1;
while (result->stderr_len + rc >= stderr_capacity) {
stderr_capacity *= 2;
char *new_buf = TS_REALLOC_PSRAM(result->stderr_data, stderr_capacity);
Expand All @@ -663,15 +674,32 @@ esp_err_t ts_ssh_exec(ts_ssh_session_t session, const char *command, ts_ssh_exec
}
memcpy(result->stderr_data + result->stderr_len, buffer, rc);
result->stderr_len += rc;
} else if (rc < 0 && rc != LIBSSH2_ERROR_EAGAIN) {
set_error(session, "Channel stderr read error: %d", rc);
ts_ssh_exec_result_free(result);
libssh2_channel_free(channel);
return ESP_FAIL;
}
} while (rc > 0);

/* 检查是否完成 */
if (rc == LIBSSH2_ERROR_EAGAIN) {
wait_socket(session->sock, session->session, 1000);
} else {
/* 用 channel EOF 判断是否结束(不再依赖 rc 的值) */
if (libssh2_channel_eof(channel)) {
break;
}

/* 收到数据则重置等待计数;无数据则累计,超过 300 轮(~5 分钟)视为超时 */
if (got_data) {
wait_rounds = 0;
} else {
wait_rounds++;
if (wait_rounds > 300) {
set_error(session, "Read timeout: no EOF after %d wait rounds", wait_rounds);
break; /* 不返回错误,保留已读数据 */
}
}

/* 未 EOF:等待更多数据到达后再循环 */
wait_socket(session->sock, session->session, 1000);
}

/* 添加字符串终止符 */
Expand Down
Loading