-
Notifications
You must be signed in to change notification settings - Fork 70
/
Copy pathorchestrator.lua
328 lines (302 loc) · 9 KB
/
orchestrator.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
-- This is a run strategy for "meta" tasks. This task itself will not perform
-- any jobs, but will instead wrap and manage a collection of other tasks.
local Task = require("overseer.task")
local constants = require("overseer.constants")
local log = require("overseer.log")
local task_list = require("overseer.task_list")
local template = require("overseer.template")
local util = require("overseer.util")
local STATUS = constants.STATUS
---@diagnostic disable-next-line: deprecated
local islist = vim.isarray or vim.tbl_islist
---Check if this is a reference to a defined task template
---@param task any
---@return boolean
local function is_named_task(task)
-- This can either be a task name, or a table with a task name as the first element
if type(task) == "string" then
return true
end
assert(type(task) == "table", "Task must be a string or table")
if islist(task) then
-- If this is a list-like table, then this is not a named task.
-- It will be a list of named tasks or task definitions.
return false
elseif type(task[1]) == "string" then
-- Named tasks have their name as the first element
return true
else
-- This is a task definition
return false
end
end
---@param tasks table
---@param cb fun(task: overseer.Task)
local function for_each_task(tasks, cb)
for _, section in ipairs(tasks) do
for _, id in ipairs(section) do
local task = task_list.get(id)
if task then
cb(task)
end
end
end
end
---@class overseer.OrchestratorStrategy : overseer.Strategy
---@field bufnr integer
---@field task_defns table[][]
---@field tasks integer[][]
local OrchestratorStrategy = {}
---Strategy for a meta-task that manage a sequence of other tasks
---@param opts table
--- tasks table A list of task definitions to run. Can include sub-lists that will be run in parallel
---@return overseer.Strategy
---@example
--- overseer.new_task({
--- name = "Build and serve app",
--- strategy = {
--- "orchestrator",
--- tasks = {
--- "make clean", -- Step 1: clean
--- { -- Step 2: build js and css in parallel
--- "npm build",
--- { cmd = {"lessc", "styles.less", "styles.css"} },
--- },
--- "npm serve", -- Step 3: serve
--- },
--- },
--- })
function OrchestratorStrategy.new(opts)
vim.validate({
opts = { opts, "t" },
})
vim.validate({
tasks = { opts.tasks, "t" },
})
-- Each entry in tasks can be either a task definition, OR a list of task definitions.
-- Convert it to each entry being a list of task definitions.
local task_defns = {}
for i, v in ipairs(opts.tasks) do
if is_named_task(v) then
task_defns[i] = { v }
elseif islist(v) then
task_defns[i] = v
else
task_defns[i] = { v }
end
end
local strategy = {
task = nil,
bufnr = vim.api.nvim_create_buf(false, true),
task_defns = task_defns,
tasks = {},
}
setmetatable(strategy, { __index = OrchestratorStrategy })
---@type overseer.OrchestratorStrategy
return strategy
end
function OrchestratorStrategy:render_buf()
if not self.bufnr or not vim.api.nvim_buf_is_valid(self.bufnr) then
return
end
local ns = vim.api.nvim_create_namespace("overseer")
vim.api.nvim_buf_clear_namespace(self.bufnr, ns, 0, -1)
local lines = {}
local highlights = {}
local columns = {}
local col_widths = {}
local max_row = 0
local function calc_width(task)
return vim.api.nvim_strwidth(task.name) + task.status:len() + 1
end
for i, task_ids in ipairs(self.tasks) do
columns[i] = vim.tbl_map(task_list.get, task_ids)
col_widths[i] = 1
for _, task in ipairs(columns[i]) do
col_widths[i] = math.max(col_widths[i], calc_width(task))
end
max_row = math.max(max_row, #columns[i])
end
for i = 1, max_row do
local line = {}
local col_start = 0
for j, column in ipairs(columns) do
local task = column[i]
if task then
table.insert(
line,
util.ljust(string.format("%s %s", task.status, task.name), col_widths[j])
)
local col_end = col_start + task.status:len()
table.insert(
highlights,
{ string.format("Overseer%s", task.status), #lines + 1, col_start, col_end }
)
else
table.insert(line, string.rep(" ", col_widths[j]))
end
col_start = col_start + line[#line]:len() + 4
end
table.insert(lines, table.concat(line, " -> "))
end
vim.bo[self.bufnr].modifiable = true
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, true, lines)
vim.bo[self.bufnr].modifiable = false
vim.bo[self.bufnr].modified = false
util.add_highlights(self.bufnr, ns, highlights)
end
function OrchestratorStrategy:reset()
self.task = nil
for_each_task(self.tasks, function(task)
task:reset()
end)
end
function OrchestratorStrategy:get_bufnr()
return self.bufnr
end
---@param task_ids integer[]
local function get_status(task_ids)
for _, v in ipairs(task_ids) do
local task = task_list.get(v)
local status = task and task.status or STATUS.FAILURE
if status ~= STATUS.SUCCESS then
return status
end
end
return STATUS.SUCCESS
end
function OrchestratorStrategy:start_next()
if self.task and not self.task:is_complete() then
local all_success = true
for i, section in ipairs(self.tasks) do
all_success = false
local status = get_status(section)
if status == STATUS.PENDING then
for _, id in ipairs(section) do
local task = task_list.get(id)
if task and task:is_pending() then
task:start()
end
end
break
elseif status == STATUS.RUNNING then
break
elseif status == STATUS.FAILURE or status == STATUS.CANCELED then
if self.task and self.task:is_running() then
self.task:finalize(status)
end
break
end
all_success = i == #self.tasks
end
if all_success then
self.task:finalize(STATUS.SUCCESS)
end
end
self:render_buf()
end
---@private
---@param defn table
---@param i integer First index into the tasks table
---@param j integer Second index into the tasks table
function OrchestratorStrategy:build_task(defn, i, j)
local search = {
dir = self.task.cwd,
}
self.tasks[i][j] = -1
---@param task overseer.Task
local function finalize_subtask(task)
task:add_component("orchestrator.on_status_broadcast")
-- Don't include child tasks when saving to bundle. We will re-create them when the
-- orchestration task is loaded.
task:set_include_in_bundle(false)
self.tasks[i][j] = task.id
if self:section_complete(1) then
self:start_next()
end
end
if type(defn) == "table" and defn[1] == nil then
defn = vim.tbl_extend("error", { parent_id = self.task.id }, defn)
local task = require("overseer").new_task(defn)
finalize_subtask(task)
return
end
local name, params = util.split_config(defn)
params = params or {}
template.get_by_name(name, search, function(tmpl)
if not tmpl then
log:error("Orchestrator could not find task '%s'", name)
self.task:finalize(STATUS.FAILURE)
return
end
local build_opts = {
search = search,
params = params,
}
template.build_task_args(
tmpl,
build_opts,
vim.schedule_wrap(function(task_defn)
if not task_defn then
log:warn("Canceled building task '%s'", name)
self.task:finalize(STATUS.FAILURE)
return
end
if params.cwd then
task_defn.cwd = params.cwd
end
if task_defn.env or params.env then
task_defn.env = vim.tbl_deep_extend("force", task_defn.env or {}, params.env or {})
end
task_defn.parent_id = self.task.id
local new_task = Task.new(task_defn)
finalize_subtask(new_task)
end)
)
end)
end
---Check if we have fully created all of the tasks in a section
---@private
---@param idx integer
function OrchestratorStrategy:section_complete(idx)
if self.task_defns[idx] == nil then
return true
end
for _, v in ipairs(self.tasks[idx]) do
if v == -1 then
return false
end
end
return vim.tbl_count(self.tasks[idx]) == vim.tbl_count(self.task_defns[idx])
end
---@param task overseer.Task
function OrchestratorStrategy:start(task)
self.task = task
task:add_component("orchestrator.on_broadcast_update_orchestrator")
for i, section in ipairs(self.task_defns) do
self.tasks[i] = self.tasks[i] or {}
for j, def in ipairs(section) do
local subtask = self.tasks[i][j] and task_list.get(self.tasks[i][j])
if not subtask or subtask:is_disposed() then
self:build_task(def, i, j)
end
end
end
if self:section_complete(1) then
vim.schedule(function()
self:start_next()
end)
end
end
function OrchestratorStrategy:stop()
for_each_task(self.tasks, function(task)
task:stop()
end)
end
function OrchestratorStrategy:dispose()
for_each_task(self.tasks, function(task)
task:dispose()
end)
util.soft_delete_buf(self.bufnr)
end
return OrchestratorStrategy