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