diff --git a/internal/lkgateway/depoevents.go b/internal/lkgateway/depoevents.go index 58db951..d892bab 100644 --- a/internal/lkgateway/depoevents.go +++ b/internal/lkgateway/depoevents.go @@ -857,26 +857,46 @@ func depoAgentJobStatus(ctx context.Context, f FancyMSSQLSettings, adminUser, ad return "подключение к msdb: " + err.Error(), false } defer mdb.Close() + // Таблицы msdb (sysjobs, sysjobhistory…) роли SQLAgentReaderRole закрыты — только представление + // sysjobs_view и процедуры sp_help_*: ими и читаем, чтобы проверка работала логином bj. var ( jobEnabled, schedEnabled, subType, subInt sql.NullInt64 runDate, runTime, runStatus sql.NullInt64 runMsg sql.NullString now time.Time ) - err = mdb.QueryRowContext(ctx, `SELECT j.enabled, s.enabled, s.freq_subday_type, s.freq_subday_interval, - h.run_date, h.run_time, h.run_status, h.message, GETDATE() - FROM msdb.dbo.sysjobs j - LEFT JOIN msdb.dbo.sysjobschedules js ON js.job_id = j.job_id - LEFT JOIN msdb.dbo.sysschedules s ON s.schedule_id = js.schedule_id - OUTER APPLY (SELECT TOP 1 run_date, run_time, run_status, message FROM msdb.dbo.sysjobhistory - WHERE job_id = j.job_id AND step_id = 0 ORDER BY instance_id DESC) h - WHERE j.name = @n`, sql.Named("n", depoJobName)).Scan(&jobEnabled, &schedEnabled, &subType, &subInt, &runDate, &runTime, &runStatus, &runMsg, &now) + err = mdb.QueryRowContext(ctx, "SELECT enabled, GETDATE() FROM msdb.dbo.sysjobs_view WHERE name = @n", sql.Named("n", depoJobName)).Scan(&jobEnabled, &now) if err == sql.ErrNoRows { return "джобы " + depoJobName + " на сервере нет — создайте её (вариант «создаёт bj» с логином администратора или скрипт для администратора)", false } if err != nil { return "список джоб недоступен логину " + conn.User + " (" + quikErrorDetail(err) + "). Чтобы bj мог проверять джобу, администратору SQL — один раз: " + strings.ReplaceAll(depoJobReaderGrant(f), "\n", " "), false } + if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobschedule @job_name = @n", sql.Named("n", depoJobName)); err == nil { + for _, r := range rows { + if mapInt(r, "enabled") == 1 || !schedEnabled.Valid { + schedEnabled = sql.NullInt64{Int64: mapInt(r, "enabled"), Valid: true} + subType = sql.NullInt64{Int64: mapInt(r, "freq_subday_type"), Valid: true} + subInt = sql.NullInt64{Int64: mapInt(r, "freq_subday_interval"), Valid: true} + } + } + } + if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_job @job_name = @n, @job_aspect = N'JOB'", sql.Named("n", depoJobName)); err == nil && len(rows) > 0 { + runDate = sql.NullInt64{Int64: mapInt(rows[0], "last_run_date"), Valid: true} + runTime = sql.NullInt64{Int64: mapInt(rows[0], "last_run_time"), Valid: true} + runStatus = sql.NullInt64{Int64: mapInt(rows[0], "last_run_outcome"), Valid: true} + } + if runStatus.Valid && runStatus.Int64 == 0 && runDate.Int64 != 0 { + // причина — в сообщении последнего выполненного шага (история идёт от новых к старым) + if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobhistory @job_name = @n, @mode = N'FULL'", sql.Named("n", depoJobName)); err == nil { + for _, r := range rows { + if mapInt(r, "step_id") > 0 { + runMsg = sql.NullString{String: fmt.Sprint(r["message"]), Valid: true} + break + } + } + } + } if jobEnabled.Int64 != 1 { return "джоба " + depoJobName + " есть, но ВЫКЛЮЧЕНА — включите её в SQL Server Agent", false } @@ -969,7 +989,7 @@ func depoJobCreateAgent(ctx context.Context, rc *RuntimeConfig, adminUser, admin } defer mdb.Close() var n int - if err := mdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM msdb.dbo.sysjobs WHERE name=@n", sql.Named("n", depoJobName)).Scan(&n); err != nil { + if err := mdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM msdb.dbo.sysjobs_view WHERE name=@n", sql.Named("n", depoJobName)).Scan(&n); err != nil { return fmt.Errorf("нет доступа к msdb (%s). Нужны права: %s", quikErrorDetail(err), strings.ReplaceAll(depoJobAgentRightsScript(f), "\n", " ")) } if n > 0 { @@ -1324,3 +1344,59 @@ func depoSimRefs(guid string, secs []IncomingSecurity) map[string]string { } return m } + +// mssqlRowMaps — строки результата (в т.ч. процедуры sp_help_*) как «колонка в нижнем регистре → значение». +func mssqlRowMaps(ctx context.Context, db *sql.DB, q string, args ...any) ([]map[string]any, error) { + rows, err := db.QueryContext(ctx, q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + cols, err := rows.Columns() + if err != nil { + return nil, err + } + var out []map[string]any + for rows.Next() { + v := make([]any, len(cols)) + p := make([]any, len(cols)) + for i := range v { + p[i] = &v[i] + } + if err := rows.Scan(p...); err != nil { + return nil, err + } + m := make(map[string]any, len(cols)) + for i, c := range cols { + m[strings.ToLower(c)] = v[i] + } + out = append(out, m) + } + return out, rows.Err() +} + +func mapInt(m map[string]any, k string) int64 { + switch v := m[k].(type) { + case int64: + return v + case int32: + return int64(v) + case int: + return int64(v) + case int16: + return int64(v) + case uint8: + return int64(v) + case bool: + if v { + return 1 + } + case []byte: + n, _ := strconv.ParseInt(string(v), 10, 64) + return n + case string: + n, _ := strconv.ParseInt(v, 10, 64) + return n + } + return 0 +}