1 /*
2  *Copyright (C) 2018 Laurent Tréguier
3  *
4  *This file is part of DLS.
5  *
6  *DLS is free software: you can redistribute it and/or modify
7  *it under the terms of the GNU General Public License as published by
8  *the Free Software Foundation, either version 3 of the License, or
9  *(at your option) any later version.
10  *
11  *DLS is distributed in the hope that it will be useful,
12  *but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *GNU General Public License for more details.
15  *
16  *You should have received a copy of the GNU General Public License
17  *along with DLS.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  */
20 
21 module dls.updater;
22 
23 private immutable changelogUrl = "https://github.com/d-language-server/dls/blob/v%s/CHANGELOG.md";
24 
25 void cleanup()
26 {
27     import dls.bootstrap : dubBinDir;
28     import dls.info : currentVersion;
29     import dub.semver : compareVersions;
30     import std.file : FileException, SpanMode, dirEntries, isSymlink, remove, rmdirRecurse;
31     import std.path : baseName;
32     import std.regex : matchFirst;
33 
34     foreach (string entry; dirEntries(dubBinDir, SpanMode.shallow))
35     {
36         const match = entry.baseName.matchFirst(`dls-v([\d.]+)`);
37 
38         if (match)
39         {
40             if (compareVersions(currentVersion, match[1]) > 0)
41             {
42                 try
43                 {
44                     rmdirRecurse(entry);
45                 }
46                 catch (FileException e)
47                 {
48                 }
49             }
50         }
51         else if (isSymlink(entry))
52         {
53             try
54             {
55                 version (Windows)
56                 {
57                     import std.file : isDir, rmdir;
58                     import std.stdio : File;
59 
60                     if (isDir(entry))
61                     {
62                         try
63                         {
64                             dirEntries(entry, SpanMode.shallow);
65                         }
66                         catch (FileException e)
67                         {
68                             rmdir(entry);
69                         }
70                     }
71                     else
72                     {
73                         try
74                         {
75                             File(entry, "rb");
76                         }
77                         catch (Exception e)
78                         {
79                             remove(entry);
80                         }
81                     }
82                 }
83                 else version (Posix)
84                 {
85                     import std.file : exists, readLink;
86 
87                     if (!exists(readLink(entry)))
88                     {
89                         remove(entry);
90                     }
91                 }
92                 else
93                 {
94                     static assert(false, "Platform not supported");
95                 }
96             }
97             catch (Exception e)
98             {
99             }
100         }
101     }
102 }
103 
104 void update(bool autoUpdate, bool preReleaseBuilds)
105 {
106     import core.time : hours;
107     import dls.bootstrap : UpgradeFailedException, canDownloadDls, downloadDls,
108         allReleases, linkDls;
109     import dls.info : currentVersion;
110     static import dls.protocol.jsonrpc;
111     import dls.protocol.interfaces.dls : DlsUpgradeSizeParams, TranslationParams;
112     import dls.protocol.logger : logger;
113     import dls.protocol.messages.methods : Dls;
114     import dls.protocol.messages.window : Util;
115     import dls.util.i18n : Tr;
116     import dub.semver : compareVersions;
117     import std.algorithm : filter, stripLeft;
118     import std.concurrency : ownerTid, receiveOnly, register, send, thisTid;
119     import std.datetime : Clock, SysTime;
120     import std.format : format;
121     import std.json : JSONType;
122 
123     auto validReleases = allReleases.filter!(
124             r => r["prerelease"].type == JSONType.false_ || preReleaseBuilds);
125 
126     if (validReleases.empty)
127     {
128         logger.warning("Unable to find any valid release");
129         return;
130     }
131 
132     immutable latestRelease = validReleases.front;
133     immutable latestVersion = latestRelease["tag_name"].str.stripLeft('v');
134     immutable releaseTime = SysTime.fromISOExtString(latestRelease["published_at"].str);
135 
136     if (latestVersion.length == 0 || compareVersions(currentVersion,
137             latestVersion) >= 0 || (Clock.currTime.toUTC() - releaseTime < 1.hours))
138     {
139         return;
140     }
141 
142     if (!autoUpdate)
143     {
144         auto id = Util.sendMessageRequest(Tr.app_upgradeDls,
145                 [Tr.app_upgradeDls_upgrade], [latestVersion, currentVersion]);
146         immutable threadName = "updater";
147         register(threadName, thisTid());
148         send(ownerTid(), Util.ThreadMessageData(id, Tr.app_upgradeDls, threadName));
149 
150         immutable shouldUpgrade = receiveOnly!bool();
151 
152         if (!shouldUpgrade)
153         {
154             return;
155         }
156     }
157 
158     dls.protocol.jsonrpc.send(Dls.UpgradeDls.didStart,
159             new TranslationParams(Tr.app_upgradeDls_upgrading));
160 
161     scope (exit)
162     {
163         dls.protocol.jsonrpc.send(Dls.UpgradeDls.didStop);
164     }
165 
166     bool upgradeSuccessful;
167 
168     if (canDownloadDls)
169     {
170         try
171         {
172             enum totalSizeCallback = (size_t size) {
173                 dls.protocol.jsonrpc.send(Dls.UpgradeDls.didChangeTotalSize,
174                         new DlsUpgradeSizeParams(Tr.app_upgradeDls_downloading, [], size));
175             };
176             enum chunkSizeCallback = (size_t size) {
177                 dls.protocol.jsonrpc.send(Dls.UpgradeDls.didChangeCurrentSize,
178                         new DlsUpgradeSizeParams(Tr.app_upgradeDls_downloading, [], size));
179             };
180             enum extractCallback = () {
181                 dls.protocol.jsonrpc.send(Dls.UpgradeDls.didExtract,
182                         new TranslationParams(Tr.app_upgradeDls_extracting));
183             };
184 
185             downloadDls(totalSizeCallback, chunkSizeCallback, extractCallback);
186             upgradeSuccessful = true;
187         }
188         catch (Exception e)
189         {
190             logger.error("Could not download DLS: %s", e.msg);
191             Util.sendMessage(Tr.app_upgradeDls_downloadError);
192         }
193     }
194 
195     try
196     {
197         linkDls();
198         auto id = Util.sendMessageRequest(Tr.app_showChangelog,
199                 [Tr.app_showChangelog_show], [latestVersion]);
200         send(ownerTid(), Util.ThreadMessageData(id, Tr.app_showChangelog,
201                 format!changelogUrl(latestVersion)));
202     }
203     catch (UpgradeFailedException e)
204     {
205         logger.error("Could not symlink DLS: %s", e.msg);
206         Util.sendMessage(Tr.app_upgradeDls_linkError);
207     }
208 }