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