OXYGENHUB
◆ main openmsx / src / fdc / DiskChanger.cc
11.7 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "DiskChanger.hh"
2
3
#include "DirAsDSK.hh"
4
#include "DiskFactory.hh"
5
#include "DiskManipulator.hh"
6
#include "DummyDisk.hh"
7
#include "RamDSKDiskImage.hh"
8
9
#include "CliComm.hh"
10
#include "CommandController.hh"
11
#include "CommandException.hh"
12
#include "EmuTime.hh"
13
#include "File.hh"
14
#include "FileContext.hh"
15
#include "FileException.hh"
16
#include "FileOperations.hh"
17
#include "FilePool.hh"
18
#include "MSXMotherBoard.hh"
19
#include "Reactor.hh"
20
#include "Scheduler.hh"
21
#include "StateChangeDistributor.hh"
22
#include "TclObject.hh"
23
#include "serialize.hh"
24
#include "serialize_constr.hh"
25
#include "serialize_stl.hh"
26
27
#include "strCat.hh"
28
29
#include <array>
30
#include <functional>
31
#include <memory>
32
#include <ranges>
33
#include <utility>
34
35
namespace openmsx {
36
37
DiskChanger::DiskChanger(MSXMotherBoard& board,
38
                         std::string driveName_,
39
                         bool createCmd,
40
                         bool doubleSidedDrive_,
41
                         unsigned nominalTracks_,
42
                         std::function<void()> preChangeCallback_)
43
	: reactor(board.getReactor())
44
	, controller(board.getCommandController())
45
	, stateChangeDistributor(&board.getStateChangeDistributor())
46
	, scheduler(&board.getScheduler())
47
	, preChangeCallback(std::move(preChangeCallback_))
48
	, driveName(std::move(driveName_))
49
	, doubleSidedDrive(doubleSidedDrive_)
50
	, nominalTracks(nominalTracks_)
51
{
52
	init(tmpStrCat(board.getMachineID(), "::"), createCmd);
53
}
54
55
DiskChanger::DiskChanger(Reactor& reactor_, std::string driveName_)
56
	: reactor(reactor_)
57
	, controller(reactor.getCommandController())
58
	, stateChangeDistributor(nullptr)
59
	, scheduler(nullptr)
60
	, driveName(std::move(driveName_))
61
	, doubleSidedDrive(true) // irrelevant, but needs a value
62
	, nominalTracks(80) // irrelevant, but needs a value
63
{
64
	init({}, true);
65
}
66
67
void DiskChanger::init(std::string_view prefix, bool createCmd)
68
{
69
	if (createCmd) createCommand();
70
	ejectDisk();
71
	auto& manipulator = reactor.getDiskManipulator();
72
	manipulator.registerDrive(*this, prefix);
73
	if (stateChangeDistributor) {
74
		stateChangeDistributor->registerListener(*this);
75
	}
76
}
77
78
void DiskChanger::createCommand()
79
{
80
	if (diskCommand) return;
81
	diskCommand.emplace(controller, *this);
82
}
83
84
DiskChanger::~DiskChanger()
85
{
86
	if (stateChangeDistributor) {
87
		stateChangeDistributor->unregisterListener(*this);
88
	}
89
	auto& manipulator = reactor.getDiskManipulator();
90
	manipulator.unregisterDrive(*this);
91
}
92
93
const DiskName& DiskChanger::getDiskName() const
94
{
95
	return disk->getName();
96
}
97
98
bool DiskChanger::diskChanged()
99
{
100
	bool ret = diskChangedFlag || disk->hasChanged();
101
	diskChangedFlag = false;
102
	return ret;
103
}
104
105
SectorAccessibleDisk* DiskChanger::getSectorAccessibleDisk()
106
{
107
	if (dynamic_cast<DummyDisk*>(disk.get())) {
108
		return nullptr;
109
	}
110
	return dynamic_cast<SectorAccessibleDisk*>(disk.get());
111
}
112
113
std::string_view DiskChanger::getContainerName() const
114
{
115
	return getDriveName();
116
}
117
118
void DiskChanger::sendChangeDiskEvent(std::span<const TclObject> args)
119
{
120
	// note: might throw MSXException
121
	if (stateChangeDistributor) {
122
		stateChangeDistributor->distributeNew<MSXCommandEvent>(
123
			scheduler->getCurrentTime(), args);
124
	} else {
125
		execute(args);
126
	}
127
}
128
129
void DiskChanger::signalStateChange(const StateChange& event)
130
{
131
	const auto* commandEvent = std::get_if<MSXCommandEvent>(&event);
132
	if (!commandEvent) return;
133
134
	execute(commandEvent->getTokens());
135
}
136
137
void DiskChanger::execute(std::span<const TclObject> tokens)
138
{
139
	if (tokens[0] == getDriveName()) {
140
		if (tokens[1] == "eject") {
141
			ejectDisk();
142
		} else {
143
			insertDisk(tokens); // might throw
144
		}
145
	}
146
}
147
148
void DiskChanger::stopReplay(EmuTime /*time*/) noexcept
149
{
150
	// nothing
151
}
152
153
int DiskChanger::insertDisk(const std::string& filename)
154
{
155
	std::array args = {TclObject("dummy"), TclObject(filename)};
156
	try {
157
		insertDisk(args);
158
		return 0;
159
	} catch (MSXException&) {
160
		return -1;
161
	}
162
}
163
164
void DiskChanger::insertDisk(std::span<const TclObject> args)
165
{
166
	std::string diskImage = FileOperations::getConventionalPath(std::string(args[1].getString()));
167
	auto& diskFactory = reactor.getDiskFactory();
168
	std::unique_ptr<Disk> newDisk(diskFactory.createDisk(diskImage, *this));
169
	for (const auto& arg : std::views::drop(args, 2)) {
170
		newDisk->applyPatch(Filename(
171
			arg.getString(), userFileContext()));
172
	}
173
174
	// no errors, only now replace original disk
175
	changeDisk(std::move(newDisk));
176
}
177
178
void DiskChanger::ejectDisk()
179
{
180
	changeDisk(std::make_unique<DummyDisk>());
181
}
182
183
void DiskChanger::changeDisk(std::unique_ptr<Disk> newDisk)
184
{
185
	if (preChangeCallback) preChangeCallback();
186
	disk = std::move(newDisk);
187
	diskChangedFlag = true;
188
	controller.getCliComm().update(CliComm::UpdateType::MEDIA, getDriveName(),
189
	                               getDiskName().getResolved());
190
}
191
192
193
// class DiskCommand
194
195
DiskCommand::DiskCommand(CommandController& commandController_,
196
                         DiskChanger& diskChanger_)
197
	: Command(commandController_, diskChanger_.driveName)
198
	, diskChanger(diskChanger_)
199
{
200
}
201
202
void DiskCommand::execute(std::span<const TclObject> tokens, TclObject& result)
203
{
204
	if (tokens.size() == 1) {
205
		result.addListElement(tmpStrCat(diskChanger.getDriveName(), ':'),
206
		                      diskChanger.getDiskName().getResolved());
207
208
		TclObject options;
209
		if (dynamic_cast<DummyDisk*>(diskChanger.disk.get())) {
210
			options.addListElement("empty");
211
		} else if (dynamic_cast<DirAsDSK*>(diskChanger.disk.get())) {
212
			options.addListElement("dirasdisk");
213
		} else if (dynamic_cast<RamDSKDiskImage*>(diskChanger.disk.get())) {
214
			options.addListElement("ramdsk");
215
		}
216
		if (diskChanger.disk->isWriteProtected()) {
217
			options.addListElement("readonly");
218
		}
219
		if (options.getListLength(getInterpreter()) != 0) {
220
			result.addListElement(options);
221
		}
222
223
	} else if (tokens[1] == "ramdsk") {
224
		std::array args = {TclObject(diskChanger.getDriveName()), tokens[1]};
225
		diskChanger.sendChangeDiskEvent(args);
226
	} else if (tokens[1] == "-ramdsk") {
227
		std::array args = {TclObject(diskChanger.getDriveName()), TclObject("ramdsk")};
228
		diskChanger.sendChangeDiskEvent(args);
229
		result = "Warning: use of '-ramdsk' is deprecated, instead use the 'ramdsk' subcommand";
230
	} else if (tokens[1] == "-eject") {
231
		std::array args = {TclObject(diskChanger.getDriveName()), TclObject("eject")};
232
		diskChanger.sendChangeDiskEvent(args);
233
		result = "Warning: use of '-eject' is deprecated, instead use the 'eject' subcommand";
234
	} else if (tokens[1] == "eject") {
235
		std::array args = {TclObject(diskChanger.getDriveName()), TclObject("eject")};
236
		diskChanger.sendChangeDiskEvent(args);
237
	} else {
238
		int firstFileToken = 1;
239
		if (tokens[1] == "insert") {
240
			if (tokens.size() > 2) {
241
				firstFileToken = 2; // skip this subcommand as file arg
242
			} else {
243
				throw CommandException("Missing argument to insert subcommand");
244
			}
245
		}
246
		try {
247
			std::vector<TclObject> args = { TclObject(diskChanger.getDriveName()) };
248
			for (size_t i = firstFileToken; i < tokens.size(); ++i) { // 'i' changes in loop
249
				std::string_view option = tokens[i].getString();
250
				if (option == "-ips") {
251
					if (++i == tokens.size()) {
252
						throw MSXException(
253
							"Missing argument for option \"", option, '\"');
254
					}
255
					args.emplace_back(tokens[i]);
256
				} else {
257
					// backwards compatibility
258
					args.emplace_back(option);
259
				}
260
			}
261
			diskChanger.sendChangeDiskEvent(args);
262
		} catch (FileException& e) {
263
			throw CommandException(std::move(e).getMessage());
264
		}
265
	}
266
}
267
268
std::string DiskCommand::help(std::span<const TclObject> /*tokens*/) const
269
{
270
	const std::string& driveName = diskChanger.getDriveName();
271
	return strCat(
272
		driveName, " eject             : remove disk from virtual drive\n",
273
		driveName, " ramdsk            : create a virtual disk in RAM\n",
274
		driveName, " insert <filename> : change the disk file\n",
275
		driveName, " <filename>        : change the disk file\n",
276
		driveName, "                   : show which disk image is in drive\n"
277
		"The following options are supported when inserting a disk image:\n"
278
		"-ips <filename> : apply the given IPS patch to the disk image");
279
}
280
281
void DiskCommand::tabCompletion(std::vector<std::string>& tokens) const
282
{
283
	if (tokens.size() >= 2) {
284
		using namespace std::literals;
285
		static constexpr std::array extra = {
286
			"eject"sv, "ramdsk"sv, "insert"sv,
287
		};
288
		completeFileName(tokens, userFileContext(), extra);
289
	}
290
}
291
292
bool DiskCommand::needRecord(std::span<const TclObject> tokens) const
293
{
294
	return tokens.size() > 1;
295
}
296
297
static Sha1Sum calcSha1(SectorAccessibleDisk* disk, FilePool& filePool)
298
{
299
	return disk ? disk->getSha1Sum(filePool) : Sha1Sum{};
300
}
301
302
// version 1:  initial version
303
// version 2:  replaced Filename with DiskName
304
template<typename Archive>
305
void DiskChanger::serialize(Archive& ar, unsigned version)
306
{
307
	DiskName diskName = disk->getName();
308
	if (ar.versionBelow(version, 2)) {
309
		// there was no DiskName yet, just a plain Filename
310
		Filename filename;
311
		ar.serialize("disk", filename);
312
		if (filename.getOriginal() == "ramdisk") {
313
			diskName = DiskName(Filename(), "ramdisk");
314
		} else {
315
			diskName = DiskName(filename, {});
316
		}
317
	} else {
318
		ar.serialize("disk", diskName);
319
	}
320
321
	std::vector<Filename> patches;
322
	if constexpr (!Archive::IS_LOADER) {
323
		patches = disk->getPatches();
324
	}
325
	ar.serialize("patches", patches);
326
327
	auto& filePool = reactor.getFilePool();
328
	Sha1Sum oldChecksum{Sha1Sum::UninitializedTag{}};
329
	if constexpr (!Archive::IS_LOADER) {
330
		oldChecksum = calcSha1(getSectorAccessibleDisk(), filePool);
331
	}
332
	ar.serialize("checksum", oldChecksum);
333
334
	if constexpr (Archive::IS_LOADER) {
335
		diskName.updateAfterLoadState();
336
		if (std::string name = diskName.getResolved(); // TODO use Filename
337
		    !name.empty()) {
338
			// Only when the original file doesn't exist on this
339
			// system, try to search by sha1sum. This means we
340
			// prefer the original file over a file with a matching
341
			// sha1sum (the original file may have changed). An
342
			// alternative is to prefer the exact sha1sum match.
343
			// I'm not sure which alternative is better.
344
			if (!FileOperations::exists(name)) {
345
				assert(!oldChecksum.empty());
346
				auto [file, filename] = filePool.getFile(
347
					FileType::DISK, oldChecksum);
348
				if (file.is_open()) {
349
					name = std::move(filename);
350
				}
351
			}
352
			std::vector<TclObject> args =
353
				{ TclObject("dummy"), TclObject(name) };
354
			for (auto& p : patches) {
355
				p.updateAfterLoadState();
356
				args.emplace_back(p.getResolved()); // TODO
357
			}
358
359
			try {
360
				insertDisk(args);
361
			} catch (MSXException& e) {
362
				throw MSXException(
363
					"Couldn't reinsert disk in drive ",
364
					getDriveName(), ": ", e.getMessage());
365
				// Alternative: Print warning and continue
366
				//   without disk image. Is this better?
367
			}
368
		}
369
370
		auto newChecksum = calcSha1(getSectorAccessibleDisk(), filePool);
371
		if (oldChecksum != newChecksum && scheduler && scheduler->getCurrentTime() != EmuTime::zero()) {
372
			controller.getCliComm().printWarning(
373
				"The content of the disk image ",
374
				diskName.getResolved(),
375
				" has changed since the time this savestate was "
376
				"created. This might result in emulation problems "
377
				"or even disk corruption. To prevent the latter, "
378
				"the disk is now write-protected (eject and "
379
				"reinsert the disk if you want to override this).");
380
			disk->forceWriteProtect();
381
		}
382
	}
383
384
	// This should only be restored after disk is inserted
385
	ar.serialize("diskChanged", diskChangedFlag);
386
}
387
388
// extra (local) constructor arguments for polymorphic de-serialization
389
template<> struct SerializeConstructorArgs<DiskChanger>
390
{
391
	using type = std::tuple<std::string>;
392
393
	template<typename Archive>
394
	void save(Archive& ar, const DiskChanger& changer) const
395
	{
396
		ar.serialize("driveName", changer.getDriveName());
397
	}
398
399
	template<typename Archive> type load(Archive& ar, unsigned /*version*/) const
400
	{
401
		std::string driveName;
402
		ar.serialize("driveName", driveName);
403
		return {driveName};
404
	}
405
};
406
407
INSTANTIATE_SERIALIZE_METHODS(DiskChanger);
408
REGISTER_POLYMORPHIC_CLASS_1(DiskContainer, DiskChanger, "DiskChanger",
409
                             std::reference_wrapper<MSXMotherBoard>);
410
411
} // namespace openmsx