OXYGENHUB
◆ main openmsx / src / events / AfterCommand.cc
17.7 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "AfterCommand.hh"
2
3
#include "CliComm.hh"
4
#include "CommandController.hh"
5
#include "CommandException.hh"
6
#include "EmuTime.hh"
7
#include "Event.hh"
8
#include "EventDistributor.hh"
9
#include "InputEventFactory.hh"
10
#include "MSXMotherBoard.hh"
11
#include "ObjectPool.hh"
12
#include "RTSchedulable.hh"
13
#include "Reactor.hh"
14
#include "Schedulable.hh"
15
#include "TclObject.hh"
16
17
#include "strCat.hh"
18
#include "StringOp.hh"
19
#include "stl.hh"
20
#include "unreachable.hh"
21
22
#include <algorithm>
23
#include <format>
24
#include <iterator>
25
#include <ranges>
26
#include <variant>
27
28
namespace openmsx {
29
30
class AfterCmd;
31
class AfterCommand;
32
33
// Helper struct to hold the essential data needed to execute a command,
34
// decoupled from the Schedulable machinery. This allows us to extract the
35
// command before destroying the Schedulable, avoiding use-after-free when
36
// the Scheduler is deleted by the executed command.
37
struct DelayedCommand {
38
	TclObject command;
39
	AfterCommand& afterCommand;
40
41
	void execute() {
42
		try {
43
			command.executeCommand(afterCommand.getInterpreter());
44
		} catch (CommandException& e) {
45
			afterCommand.getCommandController().getCliComm().printWarning(
46
				"Error executing delayed command: ", e.getMessage());
47
		}
48
	}
49
};
50
51
class AfterCmd
52
{
53
public:
54
	[[nodiscard]] const auto& getCommand() const { return command; }
55
	[[nodiscard]] auto getId() const { return id; }
56
	[[nodiscard]] auto getIdStr() const { return tmpStrCat("after#", id); }
57
	[[nodiscard]] DelayedCommand extractCommand();
58
protected:
59
	AfterCmd(AfterCommand& afterCommand,
60
		 TclObject command);
61
	AfterCommand::Index removeSelf();
62
63
	AfterCommand& afterCommand;
64
	TclObject command;
65
	const unsigned id;
66
	static inline unsigned lastAfterId = 0;
67
};
68
69
class AfterTimedCmd : public AfterCmd, private Schedulable
70
{
71
public:
72
	[[nodiscard]] double getTime() const;
73
	void reschedule();
74
protected:
75
	AfterTimedCmd(Scheduler& scheduler,
76
		      AfterCommand& afterCommand,
77
		      TclObject command, double time);
78
private:
79
	void executeUntil(EmuTime time) override;
80
	void schedulerDeleted() override;
81
82
	double time; // Zero when expired, otherwise the original duration (to
83
	             // be able to reschedule for 'after idle').
84
};
85
86
class AfterTimeCmd final : public AfterTimedCmd
87
{
88
public:
89
	AfterTimeCmd(Scheduler& scheduler,
90
		     AfterCommand& afterCommand,
91
		     TclObject command, double time);
92
};
93
94
class AfterIdleCmd final : public AfterTimedCmd
95
{
96
public:
97
	AfterIdleCmd(Scheduler& scheduler,
98
		     AfterCommand& afterCommand,
99
		     TclObject command, double time);
100
};
101
102
class AfterSimpleEventCmd final : public AfterCmd
103
{
104
public:
105
	AfterSimpleEventCmd(AfterCommand& afterCommand,
106
	                    TclObject command,
107
	                    EventType type);
108
	[[nodiscard]] std::string_view getType() const;
109
	[[nodiscard]] EventType getTypeEnum() const { return type; }
110
private:
111
	EventType type;
112
};
113
114
class AfterInputEventCmd final : public AfterCmd
115
{
116
public:
117
	AfterInputEventCmd(AfterCommand& afterCommand,
118
	                   Event event,
119
	                   TclObject command);
120
	[[nodiscard]] const Event& getEvent() const { return event; }
121
private:
122
	Event event;
123
};
124
125
class AfterRealTimeCmd final : public AfterCmd, private RTSchedulable
126
{
127
public:
128
	AfterRealTimeCmd(RTScheduler& rtScheduler, AfterCommand& afterCommand,
129
	                 TclObject command, double time);
130
131
private:
132
	void executeRT() override;
133
};
134
135
using AllAfterCmds = std::variant<AfterTimeCmd,
136
                                  AfterIdleCmd,
137
                                  AfterSimpleEventCmd,
138
                                  AfterInputEventCmd,
139
                                  AfterRealTimeCmd>;
140
static ObjectPool<AllAfterCmds> afterCmdPool;
141
142
143
AfterCommand::AfterCommand(Reactor& reactor_,
144
                           EventDistributor& eventDistributor_,
145
                           CommandController& commandController_)
146
	: Command(commandController_, "after")
147
	, reactor(reactor_)
148
	, eventDistributor(eventDistributor_)
149
{
150
	// TODO DETACHED <-> EMU types should be cleaned up
151
	//      (moved to event iso listener?)
152
	using enum EventType;
153
	for (auto type : {KEY_UP, KEY_DOWN,
154
	                  MOUSE_MOTION, MOUSE_BUTTON_UP, MOUSE_BUTTON_DOWN, MOUSE_WHEEL,
155
	                  JOY_AXIS_MOTION, JOY_HAT, JOY_BUTTON_UP, JOY_BUTTON_DOWN,
156
	                  FINISH_FRAME, BREAK, QUIT, BOOT, MACHINE_LOADED, AFTER_TIMED}) {
157
		eventDistributor.registerEventListener(type, *this);
158
	}
159
}
160
161
AfterCommand::~AfterCommand()
162
{
163
	for (auto idx : afterCmds) {
164
		afterCmdPool.remove(idx);
165
	}
166
167
	using enum EventType;
168
	for (auto type : {AFTER_TIMED, MACHINE_LOADED, BOOT, QUIT, BREAK, FINISH_FRAME,
169
	                  JOY_BUTTON_DOWN, JOY_BUTTON_UP, JOY_HAT, JOY_AXIS_MOTION, MOUSE_WHEEL,
170
	                  MOUSE_BUTTON_DOWN, MOUSE_BUTTON_UP, MOUSE_MOTION,
171
	                  KEY_DOWN, KEY_UP}) {
172
		eventDistributor.unregisterEventListener(type, *this);
173
	}
174
}
175
176
void AfterCommand::execute(std::span<const TclObject> tokens, TclObject& result)
177
{
178
	if (tokens.size() < 2) {
179
		throw CommandException("Missing argument");
180
	}
181
	std::string_view subCmd = tokens[1].getString();
182
	if (subCmd == "time") {
183
		afterTime(tokens, result);
184
	} else if (subCmd == "realtime") {
185
		afterRealTime(tokens, result);
186
	} else if (subCmd == "idle") {
187
		afterIdle(tokens, result);
188
	} else if (subCmd == "frame") {
189
		afterSimpleEvent(tokens, result, EventType::FINISH_FRAME);
190
	} else if (subCmd == "break") {
191
		afterSimpleEvent(tokens, result, EventType::BREAK);
192
	} else if (subCmd == "quit") {
193
		afterSimpleEvent(tokens, result, EventType::QUIT);
194
	} else if (subCmd == "boot") {
195
		afterSimpleEvent(tokens, result, EventType::BOOT);
196
	} else if (subCmd == "machine_switch") {
197
		afterSimpleEvent(tokens, result, EventType::MACHINE_LOADED);
198
	} else if (subCmd == "info") {
199
		afterInfo(tokens, result);
200
	} else if (subCmd == "cancel") {
201
		afterCancel(tokens, result);
202
	} else {
203
		// A valid integer?
204
		if (auto time = tokens[1].getOptionalInt()) {
205
			afterTclTime(*time, tokens, result);
206
		} else {
207
			// A valid event name?
208
			try {
209
				afterInputEvent(
210
					InputEventFactory::createInputEvent(
211
						tokens[1], getInterpreter()),
212
					tokens, result);
213
			} catch (MSXException&) {
214
				throw SyntaxError();
215
			}
216
		}
217
	}
218
}
219
220
static double getTime(Interpreter& interp, const TclObject& obj)
221
{
222
	double time = obj.getDouble(interp);
223
	if (time < 0) {
224
		throw CommandException("Not a valid time specification");
225
	}
226
	return time;
227
}
228
229
void AfterCommand::afterTime(std::span<const TclObject> tokens, TclObject& result)
230
{
231
	checkNumArgs(tokens, 4, Prefix{2}, "seconds command");
232
	MSXMotherBoard* motherBoard = reactor.getMotherBoard();
233
	if (!motherBoard) return;
234
	double time = getTime(getInterpreter(), tokens[2]);
235
	auto [idx, ptr] = afterCmdPool.emplace(
236
		std::in_place_type_t<AfterTimeCmd>{},
237
		motherBoard->getScheduler(), *this, tokens[3], time);
238
	result = std::get<AfterTimeCmd>(*ptr).getIdStr();
239
	afterCmds.push_back(idx);
240
}
241
242
void AfterCommand::afterRealTime(std::span<const TclObject> tokens, TclObject& result)
243
{
244
	checkNumArgs(tokens, 4, Prefix{2}, "seconds command");
245
	double time = getTime(getInterpreter(), tokens[2]);
246
	auto [idx, ptr] = afterCmdPool.emplace(
247
		std::in_place_type_t<AfterRealTimeCmd>{},
248
		reactor.getRTScheduler(), *this, tokens[3], time);
249
	result = std::get<AfterRealTimeCmd>(*ptr).getIdStr();
250
	afterCmds.push_back(idx);
251
}
252
253
void AfterCommand::afterTclTime(
254
	int ms, std::span<const TclObject> tokens, TclObject& result)
255
{
256
	TclObject command;
257
	command.addListElements(std::views::drop(tokens, 2));
258
	auto [idx, ptr] = afterCmdPool.emplace(
259
		std::in_place_type_t<AfterRealTimeCmd>{},
260
		reactor.getRTScheduler(), *this, command, ms * (1.0 / 1000.0));
261
	result = std::get<AfterRealTimeCmd>(*ptr).getIdStr();
262
	afterCmds.push_back(idx);
263
}
264
265
void AfterCommand::afterSimpleEvent(std::span<const TclObject> tokens, TclObject& result, EventType type)
266
{
267
	checkNumArgs(tokens, 3, "command");
268
	auto [idx, ptr] = afterCmdPool.emplace(
269
		std::in_place_type_t<AfterSimpleEventCmd>{},
270
		*this, tokens[2], type);
271
	result = std::get<AfterSimpleEventCmd>(*ptr).getIdStr();
272
	afterCmds.push_back(idx);
273
}
274
275
void AfterCommand::afterInputEvent(
276
	Event event, std::span<const TclObject> tokens, TclObject& result)
277
{
278
	checkNumArgs(tokens, 3, "command");
279
	auto [idx, ptr] = afterCmdPool.emplace(
280
		std::in_place_type_t<AfterInputEventCmd>{},
281
		*this, std::move(event), tokens[2]);
282
	result = std::get<AfterInputEventCmd>(*ptr).getIdStr();
283
	afterCmds.push_back(idx);
284
}
285
286
void AfterCommand::afterIdle(std::span<const TclObject> tokens, TclObject& result)
287
{
288
	checkNumArgs(tokens, 4, Prefix{2}, "seconds command");
289
	MSXMotherBoard* motherBoard = reactor.getMotherBoard();
290
	if (!motherBoard) return;
291
	double time = getTime(getInterpreter(), tokens[2]);
292
	auto [idx, ptr] = afterCmdPool.emplace(
293
		std::in_place_type_t<AfterIdleCmd>{},
294
		motherBoard->getScheduler(), *this, tokens[3], time);
295
	result = std::get<AfterIdleCmd>(*ptr).getIdStr();
296
	afterCmds.push_back(idx);
297
}
298
299
void AfterCommand::afterInfo(std::span<const TclObject> /*tokens*/, TclObject& result) const
300
{
301
	std::string str;
302
	for (auto idx : afterCmds) {
303
		const auto& var = afterCmdPool[idx];
304
		std::visit([&](const AfterCmd& cmd) { strAppend(str, cmd.getIdStr(), ": "); }, var);
305
		std::visit(overloaded {
306
			[&](const AfterTimeCmd&        cmd ) { str += std::format("time {:.3f} ", cmd.getTime()); },
307
			[&](const AfterIdleCmd&        cmd ) { str += std::format("idle {:.3f} ", cmd.getTime()); },
308
			[&](const AfterSimpleEventCmd& cmd ) { strAppend(str, cmd.getType(), ' '); },
309
			[&](const AfterInputEventCmd&  cmd ) { strAppend(str, toString(cmd.getEvent()), ' '); },
310
			[&](const AfterRealTimeCmd& /*cmd*/) { str += "realtime "; }
311
		}, var);
312
		std::visit([&](const AfterCmd& cmd) { strAppend(str, cmd.getCommand().getString(), '\n'); }, var);
313
	}
314
	result = str;
315
}
316
317
void AfterCommand::afterCancel(std::span<const TclObject> tokens, TclObject& /*result*/)
318
{
319
	checkNumArgs(tokens, AtLeast{3}, "id|command");
320
	if (tokens.size() == 3) {
321
		if (auto idStr = tokens[2].getString(); idStr.starts_with("after#")) {
322
			if (auto id = StringOp::stringTo<unsigned>(idStr.substr(6))) {
323
				auto equalId = [id = *id](Index idx) {
324
					return std::visit([&](const AfterCmd& cmd) {
325
						 return cmd.getId() == id;
326
					}, afterCmdPool[idx]);
327
				};
328
				if (auto it = std::ranges::find_if(afterCmds, equalId);
329
				    it != end(afterCmds)) {
330
					auto idx = *it;
331
					afterCmds.erase(it);
332
					afterCmdPool.remove(idx);
333
					return;
334
				}
335
			}
336
		}
337
	}
338
	TclObject command;
339
	command.addListElements(std::views::drop(tokens, 2));
340
	std::string_view cmdStr = command.getString();
341
	auto equalCmd = [&](Index idx) {
342
		return std::visit([&](const AfterCmd& cmd) {
343
			return cmd.getCommand() == cmdStr;
344
		}, afterCmdPool[idx]);
345
	};
346
	if (auto it = std::ranges::find_if(afterCmds, equalCmd);
347
	    it != end(afterCmds)) {
348
		auto idx = *it;
349
		afterCmds.erase(it);
350
		afterCmdPool.remove(idx);
351
		// Tcl manual is not clear about this, but it seems
352
		// there's only occurrences of this command canceled.
353
		// It's also not clear which of the (possibly) several
354
		// matches is canceled.
355
		return;
356
	}
357
	// It's not an error if no match is found
358
}
359
360
std::string AfterCommand::help(std::span<const TclObject> /*tokens*/) const
361
{
362
	return "after time     <seconds> <command>  execute a command after some time (MSX time)\n"
363
	       "after realtime <seconds> <command>  execute a command after some time (realtime)\n"
364
	       "after idle     <seconds> <command>  execute a command after some time being idle\n"
365
	       "after frame <command>               execute a command after a new frame is drawn\n"
366
	       "after break <command>               execute a command after a breakpoint is reached\n"
367
	       "after boot <command>                execute a command after a (re)boot\n"
368
	       "after machine_switch <command>      execute a command after a switch to a new machine\n"
369
	       "after quit <command>                execute a command after a quit event\n"
370
	       "after info                          list all postponed commands\n"
371
	       "after cancel <id>                   cancel the postponed command with given id\n";
372
}
373
374
void AfterCommand::tabCompletion(std::vector<std::string>& tokens) const
375
{
376
	if (tokens.size() == 2) {
377
		using namespace std::literals;
378
		static constexpr std::array cmds = {
379
			"time"sv, "realtime"sv, "idle"sv, "frame"sv, "break"sv, "boot"sv,
380
			"machine_switch"sv, "quit"sv, "info"sv, "cancel"sv,
381
		};
382
		completeString(tokens, cmds);
383
	}
384
	// TODO : make more complete
385
}
386
387
// Execute the cmds for which the predicate returns true, and erase those from afterCmds.
388
void AfterCommand::executeMatches(std::predicate<Index> auto pred)
389
{
390
	static std::vector<Index> matches; // static to keep capacity for next call
391
	assert(matches.empty());
392
393
	// Usually there are very few matches (typically even 0 or 1), so no
394
	// need to reserve() space.
395
	auto p = partition_copy_remove(afterCmds, std::back_inserter(matches), pred);
396
	afterCmds.erase(p.second, end(afterCmds));
397
	for (auto idx : matches) {
398
		auto delayed = std::visit([](AfterCmd& cmd) { return cmd.extractCommand(); },
399
		                          afterCmdPool[idx]);
400
		afterCmdPool.remove(idx);
401
		delayed.execute();
402
	}
403
	matches.clear(); // for next call (but keep capacity)
404
}
405
406
struct AfterSimpleEventPred {
407
	bool operator()(AfterCommand::Index idx) const {
408
		return std::visit(overloaded {
409
			[&](const AfterSimpleEventCmd& cmd) { return cmd.getTypeEnum() == type; },
410
			[&](const AfterCmd& /*cmd*/) { return false; }
411
		}, afterCmdPool[idx]);
412
	}
413
	const EventType type;
414
};
415
void AfterCommand::executeSimpleEvents(EventType type)
416
{
417
	executeMatches(AfterSimpleEventPred{type});
418
}
419
420
struct AfterEmuTimePred {
421
	bool operator()(AfterCommand::Index idx) const {
422
		return std::visit(overloaded {
423
			[&](const AfterTimedCmd& cmd) { return cmd.getTime() == 0.0; },
424
			[&](const AfterCmd& /*cmd*/) { return false; }
425
		}, afterCmdPool[idx]);
426
	}
427
};
428
429
struct AfterInputEventPred {
430
	explicit AfterInputEventPred(const Event& event_)
431
		: event(event_) {}
432
	bool operator()(AfterCommand::Index idx) const {
433
		return std::visit(overloaded {
434
			[&](const AfterInputEventCmd& cmd) { return matches(cmd.getEvent(), event); },
435
			[&](const AfterCmd& /*cmd*/) { return false; }
436
		}, afterCmdPool[idx]);
437
	}
438
	const Event& event;
439
};
440
441
bool AfterCommand::signalEvent(const Event& event)
442
{
443
	std::visit(overloaded{
444
		[&](const SimpleEvent&) {
445
			executeSimpleEvents(getType(event));
446
		},
447
		[&](const FinishFrameEvent&) {
448
			executeSimpleEvents(EventType::FINISH_FRAME);
449
		},
450
		[&](const QuitEvent&) {
451
			executeSimpleEvents(EventType::QUIT);
452
		},
453
		[&](const AfterTimedEvent&) {
454
			executeMatches(AfterEmuTimePred());
455
		},
456
		[&](const EventBase&) {
457
			executeMatches(AfterInputEventPred(event));
458
			for (auto idx : afterCmds) {
459
				std::visit(overloaded {
460
					[](AfterIdleCmd& cmd) { cmd.reschedule(); },
461
					[](const AfterCmd& /*cmd*/) { /*nothing*/ }
462
				}, afterCmdPool[idx]);
463
			}
464
		}
465
	}, event);
466
	return false;
467
}
468
469
470
// class AfterCmd
471
472
AfterCmd::AfterCmd(AfterCommand& afterCommand_, TclObject command_)
473
	: afterCommand(afterCommand_), command(std::move(command_)), id(++lastAfterId)
474
{
475
}
476
477
DelayedCommand AfterCmd::extractCommand()
478
{
479
	return DelayedCommand{.command = std::move(command), .afterCommand = afterCommand};
480
}
481
482
AfterCommand::Index AfterCmd::removeSelf()
483
{
484
	auto equalThis = [&](AfterCommand::Index idx) {
485
		return std::visit([&](const AfterCmd& cmd) { return &cmd == this; },
486
			          afterCmdPool[idx]);
487
	};
488
	auto it = rfind_if_unguarded(afterCommand.afterCmds, equalThis);
489
	auto idx = *it;
490
	afterCommand.afterCmds.erase(it); // move_pop_back ?
491
	return idx;
492
}
493
494
495
// class  AfterTimedCmd
496
497
AfterTimedCmd::AfterTimedCmd(
498
		Scheduler& scheduler_,
499
		AfterCommand& afterCommand_,
500
		TclObject command_, double time_)
501
	: AfterCmd(afterCommand_, std::move(command_))
502
	, Schedulable(scheduler_)
503
	, time(time_)
504
{
505
	reschedule();
506
}
507
508
double AfterTimedCmd::getTime() const
509
{
510
	return time;
511
}
512
513
void AfterTimedCmd::reschedule()
514
{
515
	removeSyncPoint();
516
	setSyncPoint(getCurrentTime() + EmuDuration::sec(time));
517
}
518
519
void AfterTimedCmd::executeUntil(EmuTime /*time*/)
520
{
521
	time = 0.0; // execute on next event
522
	afterCommand.eventDistributor.distributeEvent(AfterTimedEvent());
523
}
524
525
void AfterTimedCmd::schedulerDeleted()
526
{
527
	auto idx = removeSelf();
528
	afterCmdPool.remove(idx);
529
}
530
531
532
// class AfterTimeCmd
533
534
AfterTimeCmd::AfterTimeCmd(
535
		Scheduler& scheduler_,
536
		AfterCommand& afterCommand_,
537
		TclObject command_, double time_)
538
	: AfterTimedCmd(scheduler_, afterCommand_, std::move(command_), time_)
539
{
540
}
541
542
543
// class AfterIdleCmd
544
545
AfterIdleCmd::AfterIdleCmd(
546
		Scheduler& scheduler_,
547
		AfterCommand& afterCommand_,
548
		TclObject command_, double time_)
549
	: AfterTimedCmd(scheduler_, afterCommand_, std::move(command_), time_)
550
{
551
}
552
553
554
// class AfterSimpleEventCmd
555
556
AfterSimpleEventCmd::AfterSimpleEventCmd(
557
		AfterCommand& afterCommand_, TclObject command_,
558
		EventType type_)
559
	: AfterCmd(afterCommand_, std::move(command_)), type(type_)
560
{
561
}
562
563
std::string_view AfterSimpleEventCmd::getType() const
564
{
565
	switch (type) {
566
		using enum EventType;
567
		case FINISH_FRAME:   return "frame";
568
		case BREAK:          return "break";
569
		case BOOT:           return "boot";
570
		case QUIT:           return "quit";
571
		case MACHINE_LOADED: return "machine_switch";
572
		default: UNREACHABLE;
573
	}
574
}
575
576
577
// AfterInputEventCmd
578
579
AfterInputEventCmd::AfterInputEventCmd(
580
		AfterCommand& afterCommand_,
581
		Event event_,
582
		TclObject command_)
583
	: AfterCmd(afterCommand_, std::move(command_))
584
	, event(std::move(event_))
585
{
586
}
587
588
589
// class AfterRealTimeCmd
590
591
AfterRealTimeCmd::AfterRealTimeCmd(
592
		RTScheduler& rtScheduler, AfterCommand& afterCommand_,
593
		TclObject command_, double time)
594
	: AfterCmd(afterCommand_, std::move(command_))
595
	, RTSchedulable(rtScheduler)
596
{
597
	scheduleRT(uint64_t(time * 1e6)); // micro seconds
598
}
599
600
void AfterRealTimeCmd::executeRT()
601
{
602
	// Remove self before executing, but keep self alive till the end of
603
	// this method. Otherwise execute could execute 'after cancel ..' and
604
	// removeSelf() asserts that it can't find itself anymore.
605
	auto idx = removeSelf();
606
	auto delayed = extractCommand();
607
	afterCmdPool.remove(idx);
608
	delayed.execute();
609
}
610
611
} // namespace openmsx