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